From fb36e46f9e937c888af2f73c36e0ab5a6237114f Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Thu, 26 Oct 2023 14:17:16 -0400 Subject: [PATCH] Add new Secure Area Test App. 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 --- .../selfsigned/AddSelfSignedViewModel.kt | 20 +- gradle/libs.versions.toml | 3 + .../securearea/AndroidKeystoreSecureArea.java | 209 ++- .../android/securearea/KeystoreUtil.kt | 32 - {testapp => secure-area-test-app}/.gitignore | 0 secure-area-test-app/build.gradle | 71 + .../proguard-rules.pro | 0 .../ExampleInstrumentedTest.kt | 22 + .../src/main/AndroidManifest.xml | 11 +- .../secure_area_test_app/MainActivity.kt | 1203 +++++++++++++++++ .../secure_area_test_app/ui/theme/Color.kt | 11 + .../secure_area_test_app/ui/theme/Theme.kt | 70 + .../secure_area_test_app/ui/theme/Type.kt | 34 + .../res/drawable/ic_launcher_background.xml | 0 .../res/drawable}/ic_launcher_foreground.xml | 0 .../main/res/mipmap-anydpi}/ic_launcher.xml | 0 .../res/mipmap-anydpi}/ic_launcher_round.xml | 1 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../res/mipmap-hdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../res/mipmap-mdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin .../src/main/res/values/colors.xml | 0 .../src/main/res/values/strings.xml | 3 + .../src/main/res/values/themes.xml | 5 + .../secure_area_test_app/ExampleUnitTest.kt | 17 + settings.gradle | 2 +- testapp/build.gradle | 46 - .../testapp/ExampleInstrumentedTest.kt | 40 - .../android/identity/testapp/MainActivity.kt | 337 ----- testapp/src/main/res/layout/activity_main.xml | 81 -- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - testapp/src/main/res/values-night/themes.xml | 16 - testapp/src/main/res/values/strings.xml | 3 - testapp/src/main/res/values/themes.xml | 16 - .../identity/testapp/ExampleUnitTest.kt | 33 - 41 files changed, 1637 insertions(+), 654 deletions(-) delete mode 100644 identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt rename {testapp => secure-area-test-app}/.gitignore (100%) create mode 100644 secure-area-test-app/build.gradle rename {testapp => secure-area-test-app}/proguard-rules.pro (100%) create mode 100644 secure-area-test-app/src/androidTest/java/com/android/identity/secure_area_test_app/ExampleInstrumentedTest.kt rename {testapp => secure-area-test-app}/src/main/AndroidManifest.xml (58%) create mode 100644 secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/MainActivity.kt create mode 100644 secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt create mode 100644 secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Theme.kt create mode 100644 secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Type.kt rename {testapp => secure-area-test-app}/src/main/res/drawable/ic_launcher_background.xml (100%) rename {testapp/src/main/res/drawable-v24 => secure-area-test-app/src/main/res/drawable}/ic_launcher_foreground.xml (100%) rename {testapp/src/main/res/mipmap-anydpi-v33 => secure-area-test-app/src/main/res/mipmap-anydpi}/ic_launcher.xml (100%) rename {testapp/src/main/res/mipmap-anydpi-v26 => secure-area-test-app/src/main/res/mipmap-anydpi}/ic_launcher_round.xml (79%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-hdpi/ic_launcher.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-hdpi/ic_launcher_round.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-mdpi/ic_launcher.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-mdpi/ic_launcher_round.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-xhdpi/ic_launcher.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-xhdpi/ic_launcher_round.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-xxhdpi/ic_launcher.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-xxxhdpi/ic_launcher.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp (100%) rename {testapp => secure-area-test-app}/src/main/res/values/colors.xml (100%) create mode 100644 secure-area-test-app/src/main/res/values/strings.xml create mode 100644 secure-area-test-app/src/main/res/values/themes.xml create mode 100644 secure-area-test-app/src/test/java/com/android/identity/secure_area_test_app/ExampleUnitTest.kt delete mode 100644 testapp/build.gradle delete mode 100644 testapp/src/androidTest/java/com/android/identity/testapp/ExampleInstrumentedTest.kt delete mode 100644 testapp/src/main/java/com/android/identity/testapp/MainActivity.kt delete mode 100644 testapp/src/main/res/layout/activity_main.xml delete mode 100644 testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 testapp/src/main/res/values-night/themes.xml delete mode 100644 testapp/src/main/res/values/strings.xml delete mode 100644 testapp/src/main/res/values/themes.xml delete mode 100644 testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt index 3830842c4..bd1304aa3 100644 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt +++ b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt @@ -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 @@ -22,30 +22,30 @@ class AddSelfSignedViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - private var capabilities = KeystoreUtil.DeviceCapabilities() + private lateinit var capabilities: AndroidKeystoreSecureArea.Capabilities val screenState: StateFlow = savedStateHandle.getState( AddSelfSignedScreenState() ) fun loadConfiguration(context: Context) { - capabilities = KeystoreUtil(context).getDeviceCapabilities() + capabilities = AndroidKeystoreSecureArea.Capabilities(context) savedStateHandle.updateState { 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 ) ) } @@ -107,10 +107,10 @@ class AddSelfSignedViewModel( it.copy( useStrongBox = it.useStrongBox.copy(isEnabled = newValue), androidMdocAuthState = MdocAuthOptionState( - isEnabled = if (newValue) capabilities.strongBoxEcdh else capabilities.ecdh + isEnabled = if (newValue) capabilities.strongBoxKeyAgreementSupported else capabilities.keyAgreementSupported ), androidAuthKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (newValue) capabilities.strongBox25519 else capabilities.curve25519 + isEnabled = if (newValue) capabilities.strongBoxCurve25519Supported else capabilities.curve25519Supported ) ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5db784754..a9813dfd9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -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" } @@ -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"] 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 9b64008aa..daa040ecc 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 @@ -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; @@ -105,29 +108,8 @@ * *

Other optional features may be available depending on the version of the underlying * software (called Keymint) - * running in the Secure Area. The application may examine the - * - * FEATURE_HARDWARE_KEYSTORE and - * - * FEATURE_STRONGBOX_KEYSTORE to determine the KeyMint version for either - * the normal hardware-backed keystore and - if available - the StrongBox-backed keystore. - * - *

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 - * - * attest keys. - * - *

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. - * - *

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 - * - * KeyGuardManager.isDeviceSecure(). + * running in the Secure Area. The {@link Capabilities} helper class can be used to determine + * what the device supports. * *

This implementation works only on Android and requires API level 24 or later. */ @@ -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(); @@ -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); @@ -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); } } } @@ -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); } @@ -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 @@ -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); } @@ -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); } } @@ -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); } } } @@ -814,7 +799,7 @@ public long getUserAuthenticationTimeoutMillis() { | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) { - throw new IllegalStateException("Unexpected exception", e); + throw new IllegalStateException(e.getMessage(), e); } } @@ -1165,4 +1150,166 @@ public Builder(@NonNull byte[] attestationChallenge) { } } + + /** + * Helper class to determine capabilities of the device. + * + *

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. + * + *

Once constructed, the application may query this object to determine + * which Android Keystore features are available. + * + *

In general this is implemented by examining + * + * FEATURE_HARDWARE_KEYSTORE and + * + * FEATURE_STRONGBOX_KEYSTORE 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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; + } + } } diff --git a/identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt b/identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt deleted file mode 100644 index 3673a5862..000000000 --- a/identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.android.identity.android.securearea - -import android.content.Context -import android.os.Build - -class KeystoreUtil( - private val context: Context -) { - - fun getDeviceCapabilities(): DeviceCapabilities { - val systemAvailableFeatures = context.packageManager.systemAvailableFeatures - //TODO use the system available features to find out device capabilities - val isApiLevelOver30 = isApiLevelOver30() - return DeviceCapabilities(configureUserAuthenticationType = isApiLevelOver30) - } - - private fun isApiLevelOver30(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - } - - data class DeviceCapabilities( - val attestKey: Boolean = true, - val secureLockScreen: Boolean = true, - val ecdh: Boolean = true, - val curve25519: Boolean = true, - val strongBox: Boolean = true, - val strongBoxEcdh: Boolean = true, - val strongBox25519: Boolean = true, - val strongBoxAttestKey: Boolean = true, - val configureUserAuthenticationType: Boolean = true - ) -} diff --git a/testapp/.gitignore b/secure-area-test-app/.gitignore similarity index 100% rename from testapp/.gitignore rename to secure-area-test-app/.gitignore diff --git a/secure-area-test-app/build.gradle b/secure-area-test-app/build.gradle new file mode 100644 index 000000000..f28ab0c00 --- /dev/null +++ b/secure-area-test-app/build.gradle @@ -0,0 +1,71 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace 'com.android.identity.secure_area_test_app' + compileSdk 34 + + defaultConfig { + applicationId "com.android.identity.secure_area_test_app" + minSdk 26 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.4.6' + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } +} + +dependencies { + implementation project(':identity') + implementation project(':identity-android') + + implementation platform(libs.compose.bom) + implementation libs.bundles.androidx.core + implementation libs.bundles.androidx.lifecycle + implementation libs.bundles.androidx.navigation + implementation libs.bundles.androidx.crypto + implementation libs.bundles.androidx.activity.compose + implementation libs.bundles.bouncy.castle + implementation libs.bundles.compose + implementation libs.cbor + implementation libs.exifinterface + implementation libs.code.scanner + implementation libs.kotlinx.serialization + + androidTestImplementation libs.bundles.ui.testing + + testImplementation libs.bundles.unit.testing + + testRuntimeOnly libs.junit.jupiter.engine +} \ No newline at end of file diff --git a/testapp/proguard-rules.pro b/secure-area-test-app/proguard-rules.pro similarity index 100% rename from testapp/proguard-rules.pro rename to secure-area-test-app/proguard-rules.pro diff --git a/secure-area-test-app/src/androidTest/java/com/android/identity/secure_area_test_app/ExampleInstrumentedTest.kt b/secure-area-test-app/src/androidTest/java/com/android/identity/secure_area_test_app/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..8aae8a674 --- /dev/null +++ b/secure-area-test-app/src/androidTest/java/com/android/identity/secure_area_test_app/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.android.identity.secure_area_test_app + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Assert.assertEquals("com.android.identity.secure_area_test_app", appContext.packageName) + } +} \ No newline at end of file diff --git a/testapp/src/main/AndroidManifest.xml b/secure-area-test-app/src/main/AndroidManifest.xml similarity index 58% rename from testapp/src/main/AndroidManifest.xml rename to secure-area-test-app/src/main/AndroidManifest.xml index 420d6bff0..036743be2 100644 --- a/testapp/src/main/AndroidManifest.xml +++ b/secure-area-test-app/src/main/AndroidManifest.xml @@ -1,15 +1,20 @@ - + + android:theme="@style/Theme.IdentityCredential" + android:usesCleartextTraffic="true"> + android:exported="true" + android:label="@string/app_name" + android:enableOnBackInvokedCallback="true" + android:theme="@style/Theme.IdentityCredential"> 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 new file mode 100644 index 000000000..f7bb87d89 --- /dev/null +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/MainActivity.kt @@ -0,0 +1,1203 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.identity.secure_area_test_app + +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.FeatureInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.ConditionVariable +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.CryptoObject +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.fragment.app.FragmentActivity +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.storage.AndroidStorageEngine +import com.android.identity.internal.Util +import com.android.identity.secure_area_test_app.ui.theme.IdentityCredentialTheme +import com.android.identity.securearea.SecureArea +import com.android.identity.securearea.SecureArea.ALGORITHM_EDDSA +import com.android.identity.securearea.SecureArea.ALGORITHM_ES256 +import com.android.identity.securearea.SecureArea.ALGORITHM_ES384 +import com.android.identity.securearea.SecureArea.ALGORITHM_ES512 +import com.android.identity.securearea.SecureArea.EC_CURVE_BRAINPOOLP256R1 +import com.android.identity.securearea.SecureArea.EC_CURVE_BRAINPOOLP320R1 +import com.android.identity.securearea.SecureArea.EC_CURVE_BRAINPOOLP384R1 +import com.android.identity.securearea.SecureArea.EC_CURVE_BRAINPOOLP512R1 +import com.android.identity.securearea.SecureArea.EC_CURVE_ED25519 +import com.android.identity.securearea.SecureArea.EC_CURVE_ED448 +import com.android.identity.securearea.SecureArea.EC_CURVE_P256 +import com.android.identity.securearea.SecureArea.EC_CURVE_P384 +import com.android.identity.securearea.SecureArea.EC_CURVE_P521 +import com.android.identity.securearea.SecureArea.EC_CURVE_X25519 +import com.android.identity.securearea.SecureArea.EC_CURVE_X448 +import com.android.identity.securearea.SecureArea.KEY_PURPOSE_AGREE_KEY +import com.android.identity.securearea.SecureArea.KEY_PURPOSE_SIGN +import com.android.identity.securearea.SoftwareSecureArea +import com.android.identity.storage.EphemeralStorageEngine +import com.android.identity.util.Logger +import com.android.identity.util.Timestamp +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.File +import java.security.PrivateKey +import java.security.Security +import java.security.cert.X509Certificate +import java.time.Instant +import java.util.concurrent.Executors + +@OptIn(ExperimentalMaterial3Api::class) +class MainActivity : FragmentActivity() { + companion object { + private const val TAG = "MainActivity" + } + + private lateinit var softwareSecureArea: SoftwareSecureArea + + private lateinit var androidKeystoreCapabilities: AndroidKeystoreSecureArea.Capabilities + private lateinit var androidKeystoreSecureArea: AndroidKeystoreSecureArea + private lateinit var androidKeystoreStorage: AndroidStorageEngine + + private var keymintVersionTee: Int = 0 + private var keymintVersionStrongBox: Int = 0 + + private var executorService = Executors.newSingleThreadExecutor() + + private lateinit var sharedPreferences: SharedPreferences + + private lateinit var softwareAttestationKey: PrivateKey + private lateinit var softwareAttestationKeySignatureAlgorithm: String + private lateinit var softwareAttestationKeyCertification: List + + private fun initSoftwareAttestationKey() { + val secureArea = SoftwareSecureArea(EphemeralStorageEngine()) + val now = Timestamp.now() + secureArea.createKey( + "SoftwareAttestationRoot", + SoftwareSecureArea.CreateKeySettings.Builder("".toByteArray()) + .setEcCurve(SecureArea.EC_CURVE_P256) + .setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN) + .setSubject("CN=Software Attestation Root") + .setValidityPeriod( + now, + Timestamp.ofEpochMilli(now.toEpochMilli() + 10L * 86400 * 365 * 1000) + ) + .build() + ) + softwareAttestationKey = secureArea.getPrivateKey("SoftwareAttestationRoot", null) + softwareAttestationKeySignatureAlgorithm = "SHA256withECDSA" + softwareAttestationKeyCertification = secureArea.getKeyInfo("SoftwareAttestationRoot").attestation + } + + private fun getFeatureVersionKeystore(appContext: Context, useStrongbox: Boolean): Int { + var feature = PackageManager.FEATURE_HARDWARE_KEYSTORE + if (useStrongbox) { + feature = PackageManager.FEATURE_STRONGBOX_KEYSTORE + } + val pm = appContext.packageManager + var featureVersionFromPm = 0 + if (pm.hasSystemFeature(feature)) { + var info: FeatureInfo? = null + val infos = pm.systemAvailableFeatures + for (n in infos.indices) { + val i = infos[n] + if (i.name == feature) { + info = i + break + } + } + if (info != null) { + featureVersionFromPm = info.version + } + } + return featureVersionFromPm + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // This is needed to prefer BouncyCastle bundled with the app instead of the Conscrypt + // based implementation included in the OS itself. + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + + androidKeystoreStorage = AndroidStorageEngine.Builder( + applicationContext, + File(applicationContext.dataDir, "ic-testing") + ).build() + + androidKeystoreSecureArea = + AndroidKeystoreSecureArea( + applicationContext, + androidKeystoreStorage + ) + initSoftwareAttestationKey() + + androidKeystoreCapabilities = AndroidKeystoreSecureArea.Capabilities(applicationContext) + + softwareSecureArea = SoftwareSecureArea(androidKeystoreStorage) + + keymintVersionTee = getFeatureVersionKeystore(applicationContext, false) + keymintVersionStrongBox = getFeatureVersionKeystore(applicationContext, true) + + sharedPreferences = getSharedPreferences("default", MODE_PRIVATE) + + setContent { + ListOfSecureAreaTests() + } + } + + data class swPassphraseTestConfiguration( + val keyPurpose: Int, + val curve: Int, + val description: String + ) + + @Preview + @Composable + private fun ListOfSecureAreaTests() { + IdentityCredentialTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) + { + val showCapabilitiesDialog = remember { mutableStateOf(null) } + val showCertificateDialog = remember { mutableStateOf>(ArrayList()) } + val swShowPassphraseDialog = remember { mutableStateOf(null) } + + if (showCapabilitiesDialog.value != null) { + ShowCapabilitiesDialog( + showCapabilitiesDialog.value!!, + onDismissRequest = { + showCapabilitiesDialog.value = null + }) + } + + if (showCertificateDialog.value.size > 0) { + ShowCertificateDialog(showCertificateDialog.value, + onDismissRequest = { + showCertificateDialog.value = ArrayList() + }) + } + + if (swShowPassphraseDialog.value != null) { + ShowPassphraseDialog( + onDismissRequest = { + swShowPassphraseDialog.value = null; + }, + onContinueButtonClicked = { passphraseEnteredByUser: String -> + val configuration = swShowPassphraseDialog.value!! + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + swTest( + configuration.keyPurpose, + configuration.curve, + "1111", + passphraseEnteredByUser + ) + }) + swShowPassphraseDialog.value = null; + } + ) + } + + LazyColumn { + item { + Text( + text = "Android Keystore Secure Area", + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } + + item { + TextButton(onClick = { + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + showCapabilitiesDialog.value = androidKeystoreCapabilities + }) + }) + { + Text( + text = "Versions and Capabilities", + fontSize = 15.sp + ) + } + } + + item { + TextButton(onClick = { + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + val attestation = aksAttestation(false) + Logger.d(TAG, "attestation: " + attestation) + showCertificateDialog.value = attestation + }) + }) + { + Text( + text = "Attestation", + fontSize = 15.sp + ) + } + } + + item { + TextButton(onClick = { + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + val attestation = aksAttestation(true) + Logger.d(TAG, "attestation: " + attestation) + showCertificateDialog.value = attestation + }) + }) + { + Text( + text = "StrongBox Attestation", + fontSize = 15.sp + ) + } + } + + for ((strongBox, strongBoxDesc) in arrayOf( + Pair(false, ""), Pair(true, "StrongBox ") + )) { + for ((keyPurpose, keyPurposeDesc) in arrayOf( + Pair(KEY_PURPOSE_SIGN, "Signature"), + Pair(KEY_PURPOSE_AGREE_KEY, "Key Agreement") + )) { + for ((curve, curveName, purposes) in arrayOf( + Triple(EC_CURVE_P256, "P-256", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_ED25519, "Ed25519", KEY_PURPOSE_SIGN), + Triple(EC_CURVE_X25519, "X25519", KEY_PURPOSE_AGREE_KEY), + )) { + if ((keyPurpose and purposes) == 0) { + // No common purpose + continue + } + + val AUTH_NONE = 0 + val AUTH_LSKF_OR_BIOMETRIC = AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC + + AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF + val AUTH_LSKF_ONLY = AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF + val AUTH_BIOMETRIC_ONLY = AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC + for ((userAuthType, authTimeout, authDesc) in arrayOf( + Triple(AUTH_NONE, 0L, ""), + Triple(AUTH_LSKF_OR_BIOMETRIC, 0L, "- Auth"), + Triple(AUTH_LSKF_OR_BIOMETRIC, 10 * 1000L, "- Auth (10 sec)"), + Triple(AUTH_LSKF_ONLY, 0L, "- Auth (LSKF Only)"), + Triple(AUTH_BIOMETRIC_ONLY, 0L, "- Auth (Biometric Only)"), + Triple(AUTH_LSKF_OR_BIOMETRIC, -1L, "- Auth (No Confirmation)"), + )) { + // For brevity, Only do auth for P-256 Sign and Mac + if (curve != EC_CURVE_P256 && userAuthType != AUTH_NONE) { + continue + } + + val biometricConfirmationRequired = (authTimeout >= 0L) + item { + TextButton(onClick = { + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + aksTest( + keyPurpose, + curve, + userAuthType != AUTH_NONE, + if (authTimeout < 0L) 0L else authTimeout, + userAuthType, + biometricConfirmationRequired, + strongBox + ) + }) + }) + { + Text( + text = "$strongBoxDesc$curveName $keyPurposeDesc $authDesc", + fontSize = 15.sp + ) + } + } + } + } + } + } + + item { + Text( + text = "Software Secure Area", + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } + + item { + TextButton(onClick = { + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + val attestation = swAttestation() + Logger.d(TAG, "attestation: " + attestation) + showCertificateDialog.value = attestation + }) + }) + { + Text( + text = "Attestation", + fontSize = 15.sp + ) + } + } + + for ((keyPurpose, keyPurposeDesc) in arrayOf( + Pair(KEY_PURPOSE_SIGN, "Signature"), + Pair(KEY_PURPOSE_AGREE_KEY, "Key Agreement") + )) { + for ((curve, curveName, purposes) in arrayOf( + Triple(EC_CURVE_P256, "P-256", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_P384, "P-384", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_P521, "P-521", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_BRAINPOOLP256R1, "Brainpool 256", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_BRAINPOOLP320R1, "Brainpool 320", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_BRAINPOOLP384R1, "Brainpool 384", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_BRAINPOOLP512R1, "Brainpool 512", KEY_PURPOSE_SIGN + KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_ED25519, "Ed25519", KEY_PURPOSE_SIGN), + Triple(EC_CURVE_X25519, "X25519", KEY_PURPOSE_AGREE_KEY), + Triple(EC_CURVE_ED448, "Ed448", KEY_PURPOSE_SIGN), + Triple(EC_CURVE_X448, "X448", KEY_PURPOSE_AGREE_KEY), + )) { + if ((keyPurpose and purposes) == 0) { + // No common purpose + continue + } + for ((passphraseRequired, description) in arrayOf( + Pair(true, "- Passphrase"), + Pair(false, ""), + )) { + // For brevity, only do passphrase for first item (P-256 Signature) + if (!(keyPurpose == KEY_PURPOSE_SIGN && curve == EC_CURVE_P256)) { + if (passphraseRequired) { + continue; + } + } + + item { + TextButton(onClick = { + + if (passphraseRequired) { + swShowPassphraseDialog.value = + swPassphraseTestConfiguration(keyPurpose, curve, description) + } else { + // Does a lot of I/O, cannot run on UI thread + executorService.execute(kotlinx.coroutines.Runnable { + if (Looper.myLooper() == null) { + Looper.prepare() + } + swTest( + keyPurpose, + curve, + null, + null + ) + }) + } + }) + { + Text( + text = "$curveName $keyPurposeDesc $description", + fontSize = 15.sp + ) + } + } + } + } + } + + } + } + } + } + + @Composable + fun ShowCapabilitiesDialog(capabilities: AndroidKeystoreSecureArea.Capabilities, + onDismissRequest: () -> Unit) { + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(520.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Versions and Capabilities", + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + Column( + modifier = Modifier + .size(400.dp) + .verticalScroll(rememberScrollState()) + ) { + // Would be nice to show first API level but that's only available to tests. + Text( + text = "API Level: ${Build.VERSION.SDK_INT}\n" + + "TEE KeyMint version: ${keymintVersionTee}\n" + + "StrongBox KeyMint version: ${keymintVersionStrongBox}", + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyMedium + ) + val userAuthText = + if (capabilities.multipleAuthenticationTypesSupported) + "LSKF or Bio or LSKF+Bio" + else "Only LSKF+Bio" + val secureLockScreenText = + if (capabilities.secureLockScreenSetup) + "Enabled" + else + "Not Enabled" + Text( + text = "User Auth: $userAuthText\n" + + "Secure Lock Screen: $secureLockScreenText", + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = "Attest Key support (TEE): ${capabilities.attestKeySupported}\n" + + "Key Agreement support (TEE): ${capabilities.keyAgreementSupported}\n" + + "Curve 25519 support (TEE): ${capabilities.curve25519Supported}", + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = "StrongBox Available: ${capabilities.strongBoxSupported}\n" + + "Attest Key support (StrongBox): ${capabilities.strongBoxAttestKeySupported}\n" + + "Key Agreement support (StrongBox): ${capabilities.strongBoxKeyAgreementSupported}\n" + + "Curve 25519 support (StrongBox): ${capabilities.strongBoxCurve25519Supported}", + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyMedium + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { onDismissRequest() }, + ) { + Text("Close") + } + } + } + } + } + } + + @Composable + fun ShowCertificateDialog(attestation: List, + onDismissRequest: () -> Unit) { + var certNumber by rememberSaveable() { mutableStateOf(0) } + if (certNumber < 0 || certNumber >= attestation.size) { + certNumber = 0 + } + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(650.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Certificates", + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + Row() { + Text( + text = "Certificate ${certNumber + 1} of ${attestation.size}", + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + ) + IconButton( + enabled = (certNumber > 0), + onClick = { + certNumber -= 1 + }) { + Icon(Icons.Filled.ArrowBack, "Back") + } + IconButton( + enabled = (certNumber < attestation.size - 1), + onClick = { + certNumber += 1 + }) { + Icon(Icons.Filled.ArrowForward, "Forward") + } + } + Column( + modifier = Modifier + .size(470.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = styledX509CertificateText(attestation[certNumber].toString()), + //text = attestation[certNumber].toString(), + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyMedium + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { onDismissRequest() }, + ) { + Text("Close") + } + } + + } + } + } + } + + @Composable + private fun styledX509CertificateText(certificateText: String): AnnotatedString { + val lines = certificateText.split("\n") + return buildAnnotatedString { + for (line in lines) { + var colonPos = line.indexOf(':') + if (colonPos > 0 && line.length > (colonPos + 1) && !line[colonPos + 1].isWhitespace()) { + colonPos = -1 + } + if (colonPos > 0) { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(line.subSequence(0, colonPos)) + } + append(line.subSequence(colonPos,line.length)) + } else { + append(line) + } + append("\n") + } + } + } + + @Composable + fun ShowPassphraseDialog( + onDismissRequest: () -> Unit, + onContinueButtonClicked: (passphrase: String) -> Unit, + ) { + var passphraseTextField by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var showPassphrase by remember { mutableStateOf(value = false) } + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(275.dp) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "Enter passphrase to use key", + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + + Text( + text = "The passphrase is '1111'.", + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyMedium + ) + + TextField( + value = passphraseTextField, + maxLines = 3, + onValueChange = { passphraseTextField = it }, + textStyle = MaterialTheme.typography.bodySmall, + visualTransformation = if (showPassphrase) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + if (showPassphrase) { + IconButton(onClick = { showPassphrase = false }) { + Icon( + imageVector = Icons.Filled.Visibility, + contentDescription = "hide_password" + ) + } + } else { + IconButton( + onClick = { showPassphrase = true }) { + Icon( + imageVector = Icons.Filled.VisibilityOff, + contentDescription = "hide_password" + ) + } + } + } + ) + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { onDismissRequest() }, + ) { + Text("Cancel") + } + TextButton( + onClick = { onContinueButtonClicked(passphraseTextField.text) }, + ) { + Text("Continue") + } + } + + } + } + } + } + + private fun getNaturalAlgorithmForCurve(ecCurve: Int): Int { + return when (ecCurve) { + EC_CURVE_P256 -> ALGORITHM_ES256 + EC_CURVE_P384 -> ALGORITHM_ES384 + EC_CURVE_P521 -> ALGORITHM_ES512 + EC_CURVE_BRAINPOOLP256R1 -> ALGORITHM_ES256 + EC_CURVE_BRAINPOOLP320R1 -> ALGORITHM_ES384 + EC_CURVE_BRAINPOOLP384R1 -> ALGORITHM_ES384 + EC_CURVE_BRAINPOOLP512R1 -> ALGORITHM_ES512 + EC_CURVE_ED25519 -> ALGORITHM_EDDSA + EC_CURVE_ED448 -> ALGORITHM_EDDSA + else -> {throw IllegalStateException("Unexpected curve " + ecCurve)} + } + } + + private fun aksAttestation(strongBox: Boolean): List { + val now = Instant.now().toEpochMilli() + val thirtyDaysFromNow = Instant.now().toEpochMilli() + 30*24*3600*1000L + androidKeystoreSecureArea.createKey( + "testKey", + AndroidKeystoreSecureArea.CreateKeySettings.Builder("Challenge".toByteArray()) + .setUserAuthenticationRequired( + true, 10*1000, + AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC + + AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF + ) + .setValidityPeriod(Timestamp.ofEpochMilli(now), Timestamp.ofEpochMilli(thirtyDaysFromNow)) + .setUseStrongBox(strongBox) + .build() + ) + Logger.dHex(TAG, "encodedLeaf", androidKeystoreSecureArea.getKeyInfo("testKey").attestation[0].encoded) + return androidKeystoreSecureArea.getKeyInfo("testKey").attestation + } + + private fun aksTest( + keyPurpose: Int, + curve: Int, + authRequired: Boolean, + authTimeoutMillis: Long, + userAuthType: Int, + biometricConfirmationRequired: Boolean, + strongBox: Boolean) { + Logger.d( + TAG, + "aksTest keyPurpose:$keyPurpose curve:$curve authRequired:$authRequired authTimeout:$authTimeoutMillis strongBox:$strongBox" + ) + try { + aksTestUnguarded(keyPurpose, curve, authRequired, authTimeoutMillis, userAuthType, biometricConfirmationRequired, strongBox) + } catch (e: Exception) { + e.printStackTrace(); + Toast.makeText( + applicationContext, "${e.message}", + Toast.LENGTH_SHORT + ).show() + } + } + + private fun aksTestUnguarded( + keyPurpose: Int, + curve: Int, + authRequired: Boolean, + authTimeoutMillis: Long, + userAuthType: Int, + biometricConfirmationRequired: Boolean, + strongBox: Boolean) { + + androidKeystoreSecureArea.createKey( + "testKey", + AndroidKeystoreSecureArea.CreateKeySettings.Builder("Challenge".toByteArray()) + .setKeyPurposes(keyPurpose) + .setUserAuthenticationRequired( + authRequired, authTimeoutMillis, userAuthType) + .setUseStrongBox(strongBox) + .build() + ) + + val keyInfo = androidKeystoreSecureArea.getKeyInfo("testKey") + val publicKey = keyInfo.attestation.get(0).publicKey + + if (keyPurpose == KEY_PURPOSE_SIGN) { + val signingAlgorithm = getNaturalAlgorithmForCurve(curve) + val dataToSign = "data".toByteArray() + try { + val t0 = System.currentTimeMillis() + val derSignature = androidKeystoreSecureArea.sign( + "testKey", + signingAlgorithm, + dataToSign, + null) + val t1 = System.currentTimeMillis() + Logger.dHex( + TAG, + "Made signature with key without authentication", + derSignature + ) + Toast.makeText( + applicationContext, "Signed w/o authn (${t1 - t0} msec)", + Toast.LENGTH_SHORT + ).show() + } catch (e: SecureArea.KeyLockedException) { + val unlockData = AndroidKeystoreSecureArea.KeyUnlockData("testKey") + doUserAuth( + "Unlock to sign with key", + unlockData.getCryptoObjectForSigning(ALGORITHM_ES256), + false, + biometricConfirmationRequired, + onAuthSuccees = { + Logger.d(TAG, "onAuthSuccess") + + val t0 = System.currentTimeMillis() + val derSignature = androidKeystoreSecureArea.sign( + "testKey", + signingAlgorithm, + dataToSign, + unlockData) + val t1 = System.currentTimeMillis() + Logger.dHex( + TAG, + "Made signature with key after authentication", + derSignature + ) + Toast.makeText( + applicationContext, "Signed after authn (${t1 - t0} msec)", + Toast.LENGTH_SHORT + ).show() + }, + onAuthFailure = { + Logger.d(TAG, "onAuthFailure") + }, + onDismissed = { + Logger.d(TAG, "onDismissed") + }) + } + } else { + val otherKeyPairForEcdh = Util.createEphemeralKeyPair(curve) + try { + val t0 = System.currentTimeMillis() + val Zab = androidKeystoreSecureArea.keyAgreement( + "testKey", + otherKeyPairForEcdh.public, + null) + val t1 = System.currentTimeMillis() + Logger.dHex( + TAG, + "Calculated ECDH", + Zab) + Toast.makeText(applicationContext, "ECDH w/o authn (${t1 - t0} msec)", + Toast.LENGTH_SHORT).show() + } catch (e: SecureArea.KeyLockedException) { + val unlockData = AndroidKeystoreSecureArea.KeyUnlockData("testKey") + doUserAuth( + "Unlock to ECDH with key", + unlockData.cryptoObjectForKeyAgreement, + false, + biometricConfirmationRequired, + onAuthSuccees = { + Logger.d(TAG, "onAuthSuccess") + val t0 = System.currentTimeMillis() + val Zab = androidKeystoreSecureArea.keyAgreement( + "testKey", + otherKeyPairForEcdh.public, + unlockData) + val t1 = System.currentTimeMillis() + Logger.dHex( + TAG, + "Calculated ECDH", + Zab) + Toast.makeText(applicationContext, "ECDH after authn (${t1 - t0} msec)", + Toast.LENGTH_SHORT).show() + }, + onAuthFailure = { + Logger.d(TAG, "onAuthFailure") + }, + onDismissed = { + Logger.d(TAG, "onDismissed") + }) + } + + } + } + + // ---- + + private fun swAttestation(): List { + val now = Instant.now().toEpochMilli() + val thirtyDaysFromNow = Instant.now().toEpochMilli() + 30*24*3600*1000L + softwareSecureArea.createKey( + "testKey", + SoftwareSecureArea.CreateKeySettings.Builder("Challenge".toByteArray()) + .setValidityPeriod(Timestamp.ofEpochMilli(now), Timestamp.ofEpochMilli(thirtyDaysFromNow)) + .setAttestationKey(softwareAttestationKey, + softwareAttestationKeySignatureAlgorithm, + softwareAttestationKeyCertification) + .build() + ) + return softwareSecureArea.getKeyInfo("testKey").attestation + } + + private fun swTest( + keyPurpose: Int, + curve: Int, + passphrase: String?, + passphraseEnteredByUser: String?) { + Logger.d( + TAG, + "swTest keyPurpose:$keyPurpose curve:$curve passphrase:$passphrase" + ) + try { + swTestUnguarded(keyPurpose, curve, passphrase, passphraseEnteredByUser) + } catch (e: Exception) { + e.printStackTrace(); + Toast.makeText(applicationContext, "${e.message}", + Toast.LENGTH_SHORT).show() + } + } + + private fun swTestUnguarded( + keyPurpose: Int, + curve: Int, + passphrase: String?, + passphraseEnteredByUser: String?) { + + val builder = SoftwareSecureArea.CreateKeySettings.Builder("Challenge".toByteArray()) + .setEcCurve(curve) + .setKeyPurposes(keyPurpose) + .setAttestationKey(softwareAttestationKey, + softwareAttestationKeySignatureAlgorithm, + softwareAttestationKeyCertification) + if (passphrase != null) { + builder.setPassphraseRequired(true, passphrase) + } + softwareSecureArea.createKey("testKey", builder.build()) + + val keyInfo = softwareSecureArea.getKeyInfo("testKey") + val publicKey = keyInfo.attestation.get(0).publicKey + + var unlockData: SecureArea.KeyUnlockData? = null + if (passphraseEnteredByUser != null) { + unlockData = SoftwareSecureArea.KeyUnlockData(passphraseEnteredByUser) + } + + if (keyPurpose == KEY_PURPOSE_SIGN) { + val signingAlgorithm = getNaturalAlgorithmForCurve(curve) + try { + val t0 = System.currentTimeMillis() + val derSignature = softwareSecureArea.sign( + "testKey", + signingAlgorithm, + "data".toByteArray(), + unlockData) + val t1 = System.currentTimeMillis() + Logger.dHex( + TAG, + "Made signature with key without authentication", + derSignature + ) + Toast.makeText(applicationContext, "Signed w/o authn (${t1 - t0} msec)", + Toast.LENGTH_SHORT).show() + } catch (e: SecureArea.KeyLockedException) { + e.printStackTrace(); + Toast.makeText( + applicationContext, "${e.message}", + Toast.LENGTH_SHORT + ).show() + } + } else { + val otherKeyPairForEcdh = Util.createEphemeralKeyPair(curve) + try { + val t0 = System.currentTimeMillis() + val Zab = softwareSecureArea.keyAgreement( + "testKey", + otherKeyPairForEcdh.public, + unlockData) + val t1 = System.currentTimeMillis() + Logger.dHex( + TAG, + "Calculated ECDH without authentication", + Zab) + Toast.makeText(applicationContext, "ECDH w/o authn (${t1 - t0} msec)", + Toast.LENGTH_SHORT).show() + } catch (e: SecureArea.KeyLockedException) { + e.printStackTrace(); + Toast.makeText( + applicationContext, "${e.message}", + Toast.LENGTH_SHORT + ).show() + } + } + } + + private fun doUserAuth( + title: String, + cryptoObject: CryptoObject?, + forceLskf: Boolean, + biometricConfirmationRequired: Boolean, + onAuthSuccees: () -> Unit, + onAuthFailure: () -> Unit, + onDismissed: () -> Unit + ) { + if (Looper.getMainLooper().getThread() == Thread.currentThread()) { + throw IllegalStateException("Cannot be called from UI thread"); + } + val promptInfoBuilder = BiometricPrompt.PromptInfo.Builder() + .setTitle("Authentication required") + .setConfirmationRequired(biometricConfirmationRequired) + .setSubtitle(title) + if (forceLskf) { + // TODO: this works only on Android 11 or later but for now this is fine + // as this is just a reference/test app and this path is only hit if + // the user actually presses the "Use LSKF" button. Longer term, we should + // fall back to using KeyGuard which will work on all Android versions. + promptInfoBuilder.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + } else { + val canUseBiometricAuth = BiometricManager + .from(applicationContext) + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS + if (canUseBiometricAuth) { + promptInfoBuilder.setNegativeButtonText("Use LSKF") + } else { + promptInfoBuilder.setDeviceCredentialAllowed(true) + } + } + + var wasSuccess = false + var wasFailure = false + var wasDismissed = false + val cv = ConditionVariable() + runOnUiThread { + val biometricPromptInfo = promptInfoBuilder.build() + val activity = this as FragmentActivity + val biometricPrompt = BiometricPrompt(activity, + object : BiometricPrompt.AuthenticationCallback() { + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Logger.d(TAG, "onAuthenticationError $errorCode $errString") + if (errorCode == BiometricPrompt.ERROR_USER_CANCELED) { + wasDismissed = true + cv.open() + } else if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { + val promptInfoBuilderLskf = BiometricPrompt.PromptInfo.Builder() + .setTitle("Authentication required") + .setConfirmationRequired(biometricConfirmationRequired) + .setSubtitle(title) + promptInfoBuilderLskf.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + val biometricPromptInfoLskf = promptInfoBuilderLskf.build() + val biometricPromptLskf = BiometricPrompt(activity, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) + Logger.d(TAG, "onAuthenticationError LSKF $errorCode $errString") + if (errorCode == BiometricPrompt.ERROR_USER_CANCELED) { + wasDismissed = true + cv.open() + } else { + wasFailure = true + cv.open() + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Logger.d(TAG, "onAuthenticationSucceeded LSKF $result") + wasSuccess = true + cv.open() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Logger.d(TAG, "onAuthenticationFailed LSKF") + } + } + ) + Handler(Looper.getMainLooper()).postDelayed({ + if (cryptoObject != null) { + biometricPromptLskf.authenticate( + biometricPromptInfoLskf, cryptoObject + ) + } else { + biometricPromptLskf.authenticate(biometricPromptInfoLskf) + } + }, 100) + } else { + wasFailure = true + cv.open() + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Logger.d(TAG, "onAuthenticationSucceeded $result") + wasSuccess = true + cv.open() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Logger.d(TAG, "onAuthenticationFailed") + } + }) + Logger.d(TAG, "cryptoObject: " + cryptoObject) + if (cryptoObject != null) { + biometricPrompt.authenticate(biometricPromptInfo, cryptoObject) + } else { + biometricPrompt.authenticate(biometricPromptInfo) + } + } + cv.block() + if (wasSuccess) { + Logger.d(TAG, "Reporting success") + onAuthSuccees() + } + else if (wasFailure) { + Logger.d(TAG, "Reporting failure") + onAuthFailure() + } + else if (wasDismissed) { + Logger.d(TAG, "Reporting dismissed") + onDismissed() + } + } + +} \ No newline at end of file diff --git a/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt new file mode 100644 index 000000000..6574eded4 --- /dev/null +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.android.identity.secure_area_test_app.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Theme.kt b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Theme.kt new file mode 100644 index 000000000..86a843b60 --- /dev/null +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.android.identity.secure_area_test_app.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun IdentityCredentialTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Type.kt b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Type.kt new file mode 100644 index 000000000..71250d0bb --- /dev/null +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.android.identity.secure_area_test_app.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/testapp/src/main/res/drawable/ic_launcher_background.xml b/secure-area-test-app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from testapp/src/main/res/drawable/ic_launcher_background.xml rename to secure-area-test-app/src/main/res/drawable/ic_launcher_background.xml diff --git a/testapp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/secure-area-test-app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from testapp/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to secure-area-test-app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/testapp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/secure-area-test-app/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 100% rename from testapp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml rename to secure-area-test-app/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/secure-area-test-app/src/main/res/mipmap-anydpi/ic_launcher_round.xml similarity index 79% rename from testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to secure-area-test-app/src/main/res/mipmap-anydpi/ic_launcher_round.xml index eca70cfe5..6f3b755bf 100644 --- a/testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/secure-area-test-app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/testapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/secure-area-test-app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from testapp/src/main/res/mipmap-hdpi/ic_launcher.webp rename to secure-area-test-app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/testapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/secure-area-test-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from testapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to secure-area-test-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/testapp/src/main/res/mipmap-mdpi/ic_launcher.webp b/secure-area-test-app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from testapp/src/main/res/mipmap-mdpi/ic_launcher.webp rename to secure-area-test-app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/testapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/secure-area-test-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from testapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to secure-area-test-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/testapp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/secure-area-test-app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from testapp/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to secure-area-test-app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/secure-area-test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to secure-area-test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/testapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/secure-area-test-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from testapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to secure-area-test-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/secure-area-test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to secure-area-test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/secure-area-test-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to secure-area-test-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/secure-area-test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to secure-area-test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/testapp/src/main/res/values/colors.xml b/secure-area-test-app/src/main/res/values/colors.xml similarity index 100% rename from testapp/src/main/res/values/colors.xml rename to secure-area-test-app/src/main/res/values/colors.xml diff --git a/secure-area-test-app/src/main/res/values/strings.xml b/secure-area-test-app/src/main/res/values/strings.xml new file mode 100644 index 000000000..e577191d9 --- /dev/null +++ b/secure-area-test-app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Secure Area Test App + \ No newline at end of file diff --git a/secure-area-test-app/src/main/res/values/themes.xml b/secure-area-test-app/src/main/res/values/themes.xml new file mode 100644 index 000000000..c9a1b821b --- /dev/null +++ b/secure-area-test-app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + + - \ No newline at end of file diff --git a/testapp/src/main/res/values/strings.xml b/testapp/src/main/res/values/strings.xml deleted file mode 100644 index 7bea8042c..000000000 --- a/testapp/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Identity Library - Interactive Tests - \ No newline at end of file diff --git a/testapp/src/main/res/values/themes.xml b/testapp/src/main/res/values/themes.xml deleted file mode 100644 index 2bd778eb8..000000000 --- a/testapp/src/main/res/values/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt b/testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt deleted file mode 100644 index 7ba4a63e3..000000000 --- a/testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.identity.testapp - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file