Skip to content

Commit

Permalink
Fixes for Android Keystore Capabilities on older Android devices. (#398)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
davidz25 authored Oct 30, 2023
1 parent 4ad39f6 commit 1c0d838
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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),
Expand Down

0 comments on commit 1c0d838

Please sign in to comment.