From 1c0d838604b4388d273de13180624de0a78e1663 Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Mon, 30 Oct 2023 10:46:55 -0400 Subject: [PATCH] Fixes for Android Keystore Capabilities on older Android devices. (#398) Android devices shipping with versions before Android 12 are not required to set their KeyMint version number in the properties android.hardware.hardware_keystore and android.hardware.strongbox_keystore. Add code to work around this. In the Secure Area Test App, show name for API level and also show the First API Level and its name (the First API level is the API level the device shipped with). Also add code to work around Android Keystore creating software-backed keys if its Keymint version does not support the requested features. Test: Manually tested on Pixels 4XL, 4a, 7a, 7 Pro, 8 Pro. --- .../securearea/AndroidKeystoreSecureArea.java | 83 +++++++++++++------ .../secure_area_test_app/MainActivity.kt | 45 +++++++++- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreSecureArea.java b/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreSecureArea.java index daa040ecc..cd24f54b0 100644 --- a/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreSecureArea.java +++ b/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreSecureArea.java @@ -43,6 +43,7 @@ import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; @@ -130,6 +131,8 @@ public class AndroidKeystoreSecureArea implements SecureArea { * Flag indicating that authentication is needed using the user's biometric. */ public static final int USER_AUTHENTICATION_TYPE_BIOMETRIC = 1<<1; + private final int mKeymintTeeFeatureLevel; + private final int mKeymintSbFeatureLevel; /** * An annotation used to indicate different types of user authentication. @@ -152,6 +155,8 @@ public AndroidKeystoreSecureArea(@NonNull Context context, @NonNull StorageEngine storageEngine) { mContext = context; mStorageEngine = storageEngine; + mKeymintTeeFeatureLevel = getFeatureVersionKeystore(context, false); + mKeymintSbFeatureLevel = getFeatureVersionKeystore(context, true); } @Override @@ -179,6 +184,23 @@ public void createKey(@NonNull String alias, throw new IllegalArgumentException( "PURPOSE_AGREE_KEY not supported on this device"); } + + // Android KeyStore tries to be "helpful" by creating keys in Software if + // the Secure World (Keymint) lacks support for the requested feature, for + // example ECDH. This will never work (for RWI, the credential issuer will + // detect it when examining the attestation) so just bail early if this + // is the case. + if (aSettings.getUseStrongBox()) { + if (mKeymintSbFeatureLevel < 100) { + throw new IllegalArgumentException("PURPOSE_AGREE_KEY not supported on " + + "this StrongBox KeyMint version"); + } + } else { + if (mKeymintTeeFeatureLevel < 100) { + throw new IllegalArgumentException("PURPOSE_AGREE_KEY not supported on " + + "this KeyMint version"); + } + } } KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(alias, purposes); @@ -274,7 +296,6 @@ public void createKey(@NonNull String alias, throw new IllegalStateException(e.getMessage(), e); } kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { throw new IllegalStateException("Error creating key", e); @@ -1151,6 +1172,42 @@ public Builder(@NonNull byte[] attestationChallenge) { } + private static int getFeatureVersionKeystore(@NonNull Context appContext, boolean useStrongbox) { + String feature = PackageManager.FEATURE_HARDWARE_KEYSTORE; + if (useStrongbox) { + feature = PackageManager.FEATURE_STRONGBOX_KEYSTORE; + } + PackageManager pm = appContext.getPackageManager(); + if (pm.hasSystemFeature(feature)) { + FeatureInfo info = null; + FeatureInfo[] infos = pm.getSystemAvailableFeatures(); + for (int n = 0; n < infos.length; n++) { + FeatureInfo i = infos[n]; + if (i.name.equals(feature)) { + info = i; + break; + } + } + int version = 0; + if (info != null) { + version = info.version; + } + // It's entirely possible that the feature exists but the version number hasn't + // been set. In that case, assume it's at least KeyMaster 4.1. + if (version < 41) { + version = 41; + } + return version; + } + // It's only a requirement to set PackageManager.FEATURE_HARDWARE_KEYSTORE since + // Android 12 so for old devices this isn't set. However all devices since Android + // 8.1 has had HW-backed keystore so in this case we can report KeyMaster 4.1 + if (!useStrongbox) { + return 41; + } + return 0; + } + /** * Helper class to determine capabilities of the device. * @@ -1163,30 +1220,6 @@ public static class Capabilities { private final int mTeeFeatureLevel; private final int mSbFeatureLevel; - private static int getFeatureVersionKeystore(@NonNull Context appContext, boolean useStrongbox) { - String feature = PackageManager.FEATURE_HARDWARE_KEYSTORE; - if (useStrongbox) { - feature = PackageManager.FEATURE_STRONGBOX_KEYSTORE; - } - PackageManager pm = appContext.getPackageManager(); - int featureVersionFromPm = 0; - if (pm.hasSystemFeature(feature)) { - FeatureInfo info = null; - FeatureInfo[] infos = pm.getSystemAvailableFeatures(); - for (int n = 0; n < infos.length; n++) { - FeatureInfo i = infos[n]; - if (i.name.equals(feature)) { - info = i; - break; - } - } - if (info != null) { - featureVersionFromPm = info.version; - } - } - return featureVersionFromPm; - } - /** * Construct a new Capabilities object. * diff --git a/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/MainActivity.kt b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/MainActivity.kt index f7bb87d89..5b85db16e 100644 --- a/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/MainActivity.kt +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/MainActivity.kt @@ -163,7 +163,6 @@ class MainActivity : FragmentActivity() { feature = PackageManager.FEATURE_STRONGBOX_KEYSTORE } val pm = appContext.packageManager - var featureVersionFromPm = 0 if (pm.hasSystemFeature(feature)) { var info: FeatureInfo? = null val infos = pm.systemAvailableFeatures @@ -174,11 +173,24 @@ class MainActivity : FragmentActivity() { break } } + var version = 0 if (info != null) { - featureVersionFromPm = info.version + version = info.version } + // It's entirely possible that the feature exists but the version number hasn't + // been set. In that case, assume it's at least KeyMaster 4.1. + if (version < 41) { + version = 41 + } + return version + } + // It's only a requirement to set PackageManager.FEATURE_HARDWARE_KEYSTORE since + // Android 12 so for old devices this isn't set. However all devices since Android + // 8.1 has had HW-backed keystore so in this case we can report KeyMaster 4.1 + if (!useStrongbox) { + return 41 } - return featureVersionFromPm + return 0 } override fun onCreate(savedInstanceState: Bundle?) { @@ -504,6 +516,28 @@ class MainActivity : FragmentActivity() { } } + // Unfortunately this is API is only available to system apps so we + // have to use reflection to use it. + private fun getFirstApiLevel(): Int { + try { + val c = Class.forName("android.os.SystemProperties") + val get = c.getMethod("get", String::class.java) + val firstApiLevelString = get.invoke(c, "ro.product.first_api_level") as String + return firstApiLevelString.toInt() + } catch (e: java.lang.Exception) { + Logger.w(TAG, "Error getting ro.product.first_api_level", e) + return 0 + } + } + + private fun getNameForApiLevel(apiLevel: Int): String { + val fields = Build.VERSION_CODES::class.java.fields + var codeName = "UNKNOWN" + fields.filter { it.getInt(Build.VERSION_CODES::class) == apiLevel } + .forEach { codeName = it.name } + return codeName + } + @Composable fun ShowCapabilitiesDialog(capabilities: AndroidKeystoreSecureArea.Capabilities, onDismissRequest: () -> Unit) { @@ -530,9 +564,12 @@ class MainActivity : FragmentActivity() { .size(400.dp) .verticalScroll(rememberScrollState()) ) { + val apiLevel = Build.VERSION.SDK_INT + val firstApiLevel = getFirstApiLevel() // Would be nice to show first API level but that's only available to tests. Text( - text = "API Level: ${Build.VERSION.SDK_INT}\n" + + text = "API Level: ${apiLevel} (${getNameForApiLevel(apiLevel)})\n" + + "First API Level: ${firstApiLevel} (${getNameForApiLevel(firstApiLevel)})\n" + "TEE KeyMint version: ${keymintVersionTee}\n" + "StrongBox KeyMint version: ${keymintVersionStrongBox}", modifier = Modifier.padding(8.dp),