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..d84187ad7 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 var capabilities: AndroidKeystoreSecureArea.Capabilities? = null 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,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 + } ) ) } 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/testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt similarity index 63% rename from testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt rename to secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt index 7ba4a63e3..406a09653 100644 --- a/testapp/src/test/java/com/android/identity/testapp/ExampleUnitTest.kt +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Color.kt @@ -14,20 +14,14 @@ * limitations under the License. */ -package com.android.identity.testapp +package com.android.identity.secure_area_test_app.ui.theme -import org.junit.Test +import androidx.compose.ui.graphics.Color -import org.junit.Assert.* +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) -/** - * 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 +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..27ed28f52 --- /dev/null +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Theme.kt @@ -0,0 +1,86 @@ +/* + * 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.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..56c3e2054 --- /dev/null +++ b/secure-area-test-app/src/main/java/com/android/identity/secure_area_test_app/ui/theme/Type.kt @@ -0,0 +1,50 @@ +/* + * 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.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