Skip to content

Commit

Permalink
Add new Secure Area Test App.
Browse files Browse the repository at this point in the history
This is taken from the experimental-cloud-secure-area branch and adapted
to show just AndroidKeystoreSecureArea and SoftwareSecureArea (will rebase
that branch for all three secure areas once this is merged).

Also rework how Android Keystore capabilities are reported and show these
in the UI in the new SA test app.

Since it's now possible to test AndroidKeystoreSecureArea under various
conditions (for example when a Secure Lock Screen has not been set up)
it's easier to verify our error handling paths. To that end, fix up
propogated exceptions so they are easier to parse from the top-level
exception message and not just the cause.

Test: Manually tested
  • Loading branch information
davidz25 committed Oct 27, 2023
1 parent f4815cf commit 1f020f4
Show file tree
Hide file tree
Showing 40 changed files with 1,680 additions and 635 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.android.identity.wallet.selfsigned
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.android.identity.android.securearea.KeystoreUtil
import com.android.identity.android.securearea.AndroidKeystoreSecureArea
import com.android.identity.wallet.document.DocumentColor
import com.android.identity.wallet.document.DocumentType
import com.android.identity.wallet.document.SecureAreaImplementationState
Expand All @@ -22,30 +22,30 @@ class AddSelfSignedViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {

private var capabilities = KeystoreUtil.DeviceCapabilities()
private var capabilities: AndroidKeystoreSecureArea.Capabilities? = null

val screenState: StateFlow<AddSelfSignedScreenState> = savedStateHandle.getState(
AddSelfSignedScreenState()
)

fun loadConfiguration(context: Context) {
capabilities = KeystoreUtil(context).getDeviceCapabilities()
capabilities = AndroidKeystoreSecureArea.Capabilities(context)
savedStateHandle.updateState<AddSelfSignedScreenState> {
it.copy(
allowLSKFUnlocking = AuthTypeState(
true,
capabilities.configureUserAuthenticationType
capabilities!!.multipleAuthenticationTypesSupported
),
allowBiometricUnlocking = AuthTypeState(
true,
capabilities.configureUserAuthenticationType
capabilities!!.multipleAuthenticationTypesSupported
),
useStrongBox = AuthTypeState(false, capabilities.strongBox),
useStrongBox = AuthTypeState(false, capabilities!!.strongBoxSupported),
androidMdocAuthState = MdocAuthOptionState(
isEnabled = if (it.useStrongBox.isEnabled) capabilities.strongBoxEcdh else capabilities.ecdh
isEnabled = if (it.useStrongBox.isEnabled) capabilities!!.strongBoxKeyAgreementSupported else capabilities!!.keyAgreementSupported
),
androidAuthKeyCurveState = AndroidAuthKeyCurveState(
isEnabled = if (it.useStrongBox.isEnabled) capabilities.strongBox25519 else capabilities.curve25519
isEnabled = if (it.useStrongBox.isEnabled) capabilities!!.strongBoxCurve25519Supported else capabilities!!.curve25519Supported
)
)
}
Expand Down Expand Up @@ -107,10 +107,24 @@ class AddSelfSignedViewModel(
it.copy(
useStrongBox = it.useStrongBox.copy(isEnabled = newValue),
androidMdocAuthState = MdocAuthOptionState(
isEnabled = if (newValue) capabilities.strongBoxEcdh else capabilities.ecdh
isEnabled = if (capabilities != null) {
if (newValue)
capabilities!!.strongBoxKeyAgreementSupported
else
capabilities!!.keyAgreementSupported
} else {
false
}
),
androidAuthKeyCurveState = AndroidAuthKeyCurveState(
isEnabled = if (newValue) capabilities.strongBox25519 else capabilities.curve25519
isEnabled = if (capabilities != null) {
if (newValue)
capabilities!!.strongBoxCurve25519Supported
else
capabilities!!.curve25519Supported
} else {
false
}
)
)
}
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
kotlin = "1.8.20"
gradle-plugin = "7.4.0"
core-ktx = "1.10.1"
activity-compose = "1.8.0"
appcompat = "1.6.1"
material = "1.9.0"
constraint-layout = "2.1.4"
Expand Down Expand Up @@ -36,6 +37,7 @@

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-material = { module = "com.google.android.material:material", version.ref = "material" }
androidx-contraint-layout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraint-layout" }
Expand Down Expand Up @@ -88,6 +90,7 @@
androidx-core = ["androidx-core-ktx", "androidx-appcompat", "androidx-material", "androidx-contraint-layout", "androidx-fragment-ktx", "androidx-legacy-v4", "androidx-preference-ktx", "androidx-work"]
androidx-lifecycle = ["androidx-lifecycle-extensions", "androidx-lifecycle-livedata", "androidx-lifecycle-viewmodel"]
androidx-navigation = ["androidx-navigation-ktx", "androidx-navigation-ui-ktx"]
androidx-activity-compose = ["androidx-activity-compose"]
compose = ["compose-ui", "compose-foundation", "compose-material", "compose-ui-tooling", "compose-preview", "compose-icons"]
androidx-crypto = ["androidx-biometrics", "androidx-zxing-core"]
bouncy-castle = ["bouncy-castle-bcprov", "bouncy-castle-bcpkix"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

package com.android.identity.android.securearea;

import android.app.KeyguardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.FeatureInfo;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
Expand Down Expand Up @@ -105,29 +108,8 @@
*
* <p>Other optional features may be available depending on the version of the underlying
* software (called <a href="https://source.android.com/docs/security/features/keystore">Keymint</a>)
* running in the Secure Area. The application may examine the
* <a href="https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_HARDWARE_KEYSTORE">
* FEATURE_HARDWARE_KEYSTORE</a> and
* <a href="https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_STRONGBOX_KEYSTORE">
* FEATURE_STRONGBOX_KEYSTORE</a> to determine the KeyMint version for either
* the normal hardware-backed keystore and - if available - the StrongBox-backed keystore.
*
* <p>For Keymint 1.0 (version 100 and up), ECDH is also supported when using
* {@link SecureArea#EC_CURVE_P256}. Additionally, this version also supports
* the use of
* <a href="https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setAttestKeyAlias(java.lang.String)">
* attest keys</a>.
*
* <p>For Keymint 2.0 (version 200 and up), curves {@link #EC_CURVE_ED25519} is available
* for {@link #KEY_PURPOSE_SIGN} keys and curve {@link #EC_CURVE_X25519} is available for
* {@link #KEY_PURPOSE_AGREE_KEY} keys.
*
* <p>If the device has a secure lock screen (either PIN, pattern, or password) this can
* be used to protect keys using
* {@link CreateKeySettings.Builder#setUserAuthenticationRequired(boolean, long, int)}.
* The application can test for whether the lock screen is configured using
* <a href="https://developer.android.com/reference/android/app/KeyguardManager#isDeviceSecure()">
* KeyGuardManager.isDeviceSecure()</a>.
* running in the Secure Area. The {@link Capabilities} helper class can be used to determine
* what the device supports.
*
* <p>This implementation works only on Android and requires API level 24 or later.
*/
Expand Down Expand Up @@ -289,7 +271,7 @@ public void createKey(@NonNull String alias,
try {
kpg.initialize(builder.build());
} catch (InvalidAlgorithmParameterException e) {
throw new IllegalStateException("Unexpected exception", e);
throw new IllegalStateException(e.getMessage(), e);
}
kpg.generateKeyPair();

Expand Down Expand Up @@ -370,6 +352,9 @@ static String getSignatureAlgorithmName(@Algorithm int signatureAlgorithm) {
case ALGORITHM_ES512:
return "SHA512withECDSA";

case ALGORITHM_EDDSA:
return "Ed25519";

default:
throw new IllegalArgumentException(
"Unsupported signing algorithm with id " + signatureAlgorithm);
Expand Down Expand Up @@ -403,7 +388,7 @@ static String getSignatureAlgorithmName(@Algorithm int signatureAlgorithm) {
unlockData.mSignature.update(dataToSign);
return unlockData.mSignature.sign();
} catch (SignatureException e) {
throw new IllegalStateException("Unexpected exception while signing", e);
throw new IllegalStateException(e.getMessage(), e);
}
}
}
Expand Down Expand Up @@ -436,7 +421,7 @@ static String getSignatureAlgorithmName(@Algorithm int signatureAlgorithm) {
"android.security.KeyStoreException: Key user not authenticated")) {
throw new KeyLockedException("User not authenticated", e);
}
throw new IllegalStateException("Unexpected exception while signing", e);
throw new IllegalStateException(e.getMessage(), e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Key does not have purpose KEY_PURPOSE_SIGN", e);
}
Expand Down Expand Up @@ -467,7 +452,7 @@ static String getSignatureAlgorithmName(@Algorithm int signatureAlgorithm) {
| IOException
| NoSuchAlgorithmException
| NoSuchProviderException e) {
throw new IllegalStateException("Unexpected exception while doing key agreement", e);
throw new IllegalStateException(e.getMessage(), e);
} catch (ProviderException e) {
// This is a work-around for Android Keystore throwing a ProviderException
// when it should be throwing UserNotAuthenticatedException instead. b/282174161
Expand All @@ -476,7 +461,7 @@ static String getSignatureAlgorithmName(@Algorithm int signatureAlgorithm) {
&& e.getCause().getMessage().startsWith("Key user not authenticated")) {
throw new KeyLockedException("User not authenticated", e);
}
throw new IllegalStateException("Unexpected exception while doing key agreement", e);
throw new IllegalStateException(e.getMessage(), e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Key does not have purpose KEY_PURPOSE_AGREE_KEY", e);
}
Expand Down Expand Up @@ -554,7 +539,7 @@ public KeyUnlockData(@NonNull String alias) {
| NoSuchAlgorithmException
| InvalidKeyException
| NoSuchProviderException e) {
throw new IllegalStateException("Unexpected exception", e);
throw new IllegalStateException(e.getMessage(), e);
}
}

Expand Down Expand Up @@ -608,7 +593,7 @@ public KeyUnlockData(@NonNull String alias) {
| IOException
| NoSuchAlgorithmException
| NoSuchProviderException e) {
throw new IllegalStateException("Unexpected exception", e);
throw new IllegalStateException(e.getMessage(), e);
}
}
}
Expand Down Expand Up @@ -814,7 +799,7 @@ public long getUserAuthenticationTimeoutMillis() {
| NoSuchAlgorithmException
| NoSuchProviderException
| InvalidKeySpecException e) {
throw new IllegalStateException("Unexpected exception", e);
throw new IllegalStateException(e.getMessage(), e);
}
}

Expand Down Expand Up @@ -1165,4 +1150,166 @@ public Builder(@NonNull byte[] attestationChallenge) {
}

}

/**
* Helper class to determine capabilities of the device.
*
* <p>This class can be used by applications to determine the extent of
* Android Keystore support on the device the application is running on.
*/
public static class Capabilities {
private final KeyguardManager mKeyguardManager;
private final int mApiLevel;
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.
*
* <p>Once constructed, the application may query this object to determine
* which Android Keystore features are available.
*
* <p>In general this is implemented by examining
* <a href="https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_HARDWARE_KEYSTORE">
* FEATURE_HARDWARE_KEYSTORE</a> and
* <a href="https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_STRONGBOX_KEYSTORE">
* FEATURE_STRONGBOX_KEYSTORE</a> to determine the KeyMint version for both
* the normal hardware-backed keystore and - if available - the StrongBox-backed keystore.
*
* @param context the application context.
*/
public Capabilities(@NonNull Context context) {
mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
mTeeFeatureLevel = getFeatureVersionKeystore(context, false);
mSbFeatureLevel = getFeatureVersionKeystore(context, true);
mApiLevel = Build.VERSION.SDK_INT;
}

/**
* Gets whether a Secure Lock Screen has been set up.
*
* <p>This checks whether the device currently has a secure lock
* screen (either PIN, pattern, or password).
*
* @return {@code true} if Secure Lock Screen has been set up, {@link false} otherwise.
*/
public boolean getSecureLockScreenSetup() {
return mKeyguardManager.isDeviceSecure();
}

/**
* Gets whether it's possible to specify multiple authentication types.
*
* <p>On Android versions before API 30 (Android 11), it's not possible to specify whether
* LSKF or Biometric or both can be used to unlock a key (both are always possible).
* Starting with Android 11, it's possible to specify all three combinations (LSKF only,
* Biometric only, or both).
*
* @return {@code true} if possible to use multiple authentication types, {@link false} otherwise.
*/
public boolean getMultipleAuthenticationTypesSupported() {
return mApiLevel >= Build.VERSION_CODES.R;
}

/**
* Gets whether Attest Keys are supported.
*
* <p>This is only supported in KeyMint 1.0 (version 100) and higher.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getAttestKeySupported() {
return mTeeFeatureLevel >= 100;
}

/**
* Gets whether Key Agreement is supported.
*
* <p>This is only supported in KeyMint 1.0 (version 100) and higher.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getKeyAgreementSupported() {
return mTeeFeatureLevel >= 100;
}

/**
* Gets whether Curve25519 is supported.
*
* <p>This is only supported in KeyMint 2.0 (version 200) and higher.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getCurve25519Supported() {
return mTeeFeatureLevel >= 200;
}

/**
* Gets whether StrongBox is supported.
*
* <p>StrongBox requires dedicated hardware and is not available on all devices.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getStrongBoxSupported() {
return mSbFeatureLevel > 0;
}

/**
* Gets whether StrongBox Attest Keys are supported.
*
* <p>This is only supported in StrongBox KeyMint 1.0 (version 100) and higher.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getStrongBoxAttestKeySupported() {
return mSbFeatureLevel >= 100;
}

/**
* Gets whether StrongBox Key Agreement is supported.
*
* <p>This is only supported in StrongBox KeyMint 1.0 (version 100) and higher.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getStrongBoxKeyAgreementSupported() {
return mSbFeatureLevel >= 100;
}

/**
* Gets whether StrongBox Curve25519 is supported.
*
* <p>This is only supported in StrongBox KeyMint 2.0 (version 200) and higher.
*
* @return {@code true} if supported, {@link false} otherwise.
*/
public boolean getStrongBoxCurve25519Supported() {
return mSbFeatureLevel >= 200;
}
}
}
Loading

0 comments on commit 1f020f4

Please sign in to comment.