diff --git a/appholder/.gitignore b/appholder/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/appholder/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/appholder/build.gradle.kts b/appholder/build.gradle.kts deleted file mode 100644 index e31b4e9e8..000000000 --- a/appholder/build.gradle.kts +++ /dev/null @@ -1,114 +0,0 @@ -plugins { - alias(libs.plugins.androidApplication) - alias(libs.plugins.compose.compiler) - alias(libs.plugins.jetbrainsCompose) - id("kotlin-android") - alias(libs.plugins.navigation.safe.args) - alias(libs.plugins.parcelable) - alias(libs.plugins.kapt) -} - -val projectVersionCode: Int by rootProject.extra -val projectVersionName: String by rootProject.extra - -kotlin { - jvmToolchain(17) -} - -android { - namespace = "com.android.identity.wallet" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - defaultConfig { - applicationId = "com.android.identity.wallet" - minSdk = 29 - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = projectVersionCode - versionName = projectVersionName - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = true - isShrinkResources = true - } - } - - flavorDimensions.addAll(listOf("standard")) - productFlavors { - create("wallet") { - dimension = "standard" - isDefault = true - } - create("purse") { - dimension = "standard" - applicationIdSuffix = ".purse" - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - buildFeatures { - dataBinding = true - viewBinding = true - compose = true - } - - packaging { - resources { - excludes += listOf("/META-INF/{AL2.0,LGPL2.1}") - excludes += listOf("/META-INF/versions/9/OSGI-INF/MANIFEST.MF") - } - } -} - -dependencies { - implementation(project(":identity")) - implementation(project(":identity-mdoc")) - implementation(project(":identity-android")) - implementation(project(":identity-doctypes")) - implementation(project(":jpeg2k")) - - implementation(libs.kotlinx.datetime) - - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(compose.material) - - debugImplementation(compose.uiTooling) - implementation(compose.preview) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.biometrics) - implementation(compose.material3) - implementation(libs.compose.material.icons.extended) - implementation(libs.androidx.navigation.runtime) - implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.activity.compose) - implementation(libs.code.scanner) - implementation(libs.androidx.material) - implementation(libs.androidx.navigation.fragment) - implementation(libs.androidx.navigation.ui) - implementation(libs.androidx.preference) - implementation(libs.kotlinx.io.core) - implementation(libs.cbor) - implementation(libs.exifinterface) - implementation(libs.androidx.work) - - implementation(files("../third-party/play-services-identity-credentials-0.0.1-eap01.aar")) - implementation(libs.bundles.google.play.services) - - implementation(libs.bouncy.castle.bcprov) - implementation(libs.bouncy.castle.bcpkix) - - testImplementation(libs.kotlin.test) - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.espresso.core) -} diff --git a/appholder/lint.xml b/appholder/lint.xml deleted file mode 100644 index e06ca9c94..000000000 --- a/appholder/lint.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt b/appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt deleted file mode 100644 index 2d948e4c0..000000000 --- a/appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.mdl.app - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert -import org.junit.Assert.assertEquals -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.assertTrue( - "com.android.identity.wallet" == appContext.packageName || - "com.android.identity.wallet.purse" == appContext.packageName - ) - } -} \ No newline at end of file diff --git a/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt b/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt deleted file mode 100644 index b9a1a850f..000000000 --- a/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.android.mdl.app.selfsigned - -import androidx.lifecycle.SavedStateHandle -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.storage.AndroidStorageEngine -import com.android.identity.documenttype.knowntypes.EUPersonalID -import com.android.identity.documenttype.knowntypes.VehicleRegistration -import com.android.identity.securearea.SecureAreaRepository -import com.android.identity.securearea.software.SoftwareSecureArea -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState -import com.android.identity.wallet.selfsigned.AddSelfSignedViewModel -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.ProvisioningUtil -import kotlinx.io.files.Path -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SelfSignedScreenStateTest { - - private val savedStateHandle = SavedStateHandle() - private lateinit var repository: SecureAreaRepository - - @Before - fun setUp() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - repository = ProvisioningUtil.getInstance(context).secureAreaRepository - val storageFile = Path(PreferencesHelper.getKeystoreBackedStorageLocation(context).path) - val storageEngine = AndroidStorageEngine.Builder(context, storageFile).build() - val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine) - val softwareSecureArea = SoftwareSecureArea(storageEngine) - repository.addImplementation(androidKeystoreSecureArea) - repository.addImplementation(softwareSecureArea) - } - - @Test - fun defaultScreenState() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState()) - } - - @Test - fun updateDocumentType() { - val personalId = EUPersonalID.getDocumentType().mdocDocumentType?.docType!! - val name= EUPersonalID.getDocumentType().displayName - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateDocumentType(personalId, name) - - assertEquals(viewModel.screenState.value, - AddSelfSignedScreenState(documentType = personalId, documentName = "EU Personal ID") - ) - } - - @Test - fun updateDocumentName() { - val newName = ":irrelevant:" - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateDocumentName(newName) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(documentName = newName)) - } - - @Test - fun updateDocumentTypeAfterNameUpdate() { - val registration = VehicleRegistration.getDocumentType().mdocDocumentType?.docType!! - val name = VehicleRegistration.getDocumentType().displayName - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateDocumentName(":irrelevant:") - viewModel.updateDocumentType(registration, name) - - assertEquals(viewModel.screenState.value, - AddSelfSignedScreenState( - documentType = registration, - documentName = "Vehicle Registration" - ) - ) - } - - @Test - fun updateCardArt() { - val blue = DocumentColor.Blue - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateCardArt(blue) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(cardArt = blue)) - } - - @Test - fun updateValidityInDays() { - val newValue = 15 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateValidityInDays(newValue) - - assertEquals(viewModel.screenState.value.validityInDays, newValue) - } - - @Test - fun updateValidityInDaysBelowMinValidityDays() { - val defaultMinValidity = 10 - val belowMinValidity = 9 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateValidityInDays(defaultMinValidity) - viewModel.updateValidityInDays(belowMinValidity) - - assertEquals(viewModel.screenState.value.validityInDays, defaultMinValidity) - } - - @Test - fun updateMinValidityInDays() { - val newMinValidity = 15 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMinValidityInDays(newMinValidity) - - assertEquals(viewModel.screenState.value.minValidityInDays, newMinValidity) - } - - @Test - fun updateMinValidityInDaysAboveValidityInDays() { - val defaultValidityInDays = 30 - val minValidityInDays = defaultValidityInDays + 5 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMinValidityInDays(minValidityInDays) - - assertEquals(viewModel.screenState.value.validityInDays, minValidityInDays) - } - - @Test - fun updateNumberOfMso() { - val msoCount = 2 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateNumberOfMso(msoCount) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(numberOfMso = msoCount)) - } - - @Test - fun updateNumberOfMsoInvalidValue() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateNumberOfMso(1) - viewModel.updateNumberOfMso(0) - viewModel.updateNumberOfMso(-1) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(numberOfMso = 1)) - } - - @Test - fun updateMaxUseOfMso() { - val maxMsoUsages = 3 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMaxUseOfMso(maxMsoUsages) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(maxUseOfMso = maxMsoUsages)) - } - - @Test - fun updateMaxUseOfMsoInvalidValue() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMaxUseOfMso(1) - viewModel.updateMaxUseOfMso(0) - viewModel.updateMaxUseOfMso(-1) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(maxUseOfMso = 1)) - } -} \ No newline at end of file diff --git a/appholder/src/main/AndroidManifest.xml b/appholder/src/main/AndroidManifest.xml deleted file mode 100644 index 950ed3ef4..000000000 --- a/appholder/src/main/AndroidManifest.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/appholder/src/main/assets/identitycredentialmatcher.wasm b/appholder/src/main/assets/identitycredentialmatcher.wasm deleted file mode 100644 index 027d559b7..000000000 Binary files a/appholder/src/main/assets/identitycredentialmatcher.wasm and /dev/null differ diff --git a/appholder/src/main/ic_launcher-playstore.png b/appholder/src/main/ic_launcher-playstore.png deleted file mode 100644 index b01a66a30..000000000 Binary files a/appholder/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt b/appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt deleted file mode 100644 index f03a27856..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt +++ /dev/null @@ -1,359 +0,0 @@ -package com.android.identity.wallet - -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Base64 -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.fragment.app.FragmentActivity -import com.android.identity.android.mdoc.util.CredmanUtil -import com.android.identity.android.securearea.AndroidKeystoreKeyUnlockData -import com.android.identity.document.DocumentRequest -import com.android.identity.document.NameSpacedData -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcCurve -import com.android.identity.crypto.EcPublicKeyDoubleCoordinate -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.mdoc.mso.StaticAuthDataParser -import com.android.identity.mdoc.response.DeviceResponseGenerator -import com.android.identity.mdoc.response.DocumentGenerator -import com.android.identity.mdoc.util.MdocUtil -import com.android.identity.securearea.KeyLockedException -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.util.Constants -import com.android.identity.util.Logger -import com.android.identity.wallet.util.ProvisioningUtil -import com.android.identity.wallet.util.log -import com.google.android.gms.identitycredentials.GetCredentialResponse -import com.google.android.gms.identitycredentials.IntentHelper -import com.google.android.gms.identitycredentials.IntentHelper.EXTRA_CREDENTIAL_ID -import com.google.android.gms.identitycredentials.IntentHelper.extractGetCredentialRequest -import com.google.android.gms.identitycredentials.IntentHelper.setGetCredentialException -import com.google.android.gms.identitycredentials.IntentHelper.setGetCredentialResponse -import org.json.JSONObject -import java.util.StringTokenizer -import kotlinx.datetime.Clock - -class GetCredentialActivity : FragmentActivity() { - - fun addDeviceNamespaces(documentGenerator : DocumentGenerator, - credential : MdocCredential, - unlockData: KeyUnlockData?) { - documentGenerator.setDeviceNamespacesSignature( - NameSpacedData.Builder().build(), - credential.secureArea, - credential.alias, - unlockData, - Algorithm.ES256) - } - - fun doBiometricAuth(credential : MdocCredential, - forceLskf : Boolean, - onBiometricAuthCompleted: (unlockData: KeyUnlockData?) -> Unit) { - var title = "To share your credential we need to check that it's you." - var unlockData = AndroidKeystoreKeyUnlockData(credential.alias) - var cryptoObject = unlockData.getCryptoObjectForSigning(Algorithm.ES256) - - val promptInfoBuilder = BiometricPrompt.PromptInfo.Builder() - .setTitle("Authentication required") - .setSubtitle(title) - .setConfirmationRequired(false) - 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 PIN") - } else { - promptInfoBuilder.setDeviceCredentialAllowed(true) - } - } - - val biometricPromptInfo = promptInfoBuilder.build() - val biometricPrompt = BiometricPrompt(this, - object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { - Logger.d("TAG", "onAuthenticationError $errorCode $errString") - // TODO: "Use LSKF"... without this delay, the prompt won't work correctly - Handler(Looper.getMainLooper()).postDelayed({ - doBiometricAuth(credential, true, onBiometricAuthCompleted) - }, 100) - } - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - Logger.d("TAG", "onAuthenticationSucceeded $result") - - onBiometricAuthCompleted(unlockData) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Logger.d("TAG", "onAuthenticationFailed") - } - }) - - if (cryptoObject != null) { - biometricPrompt.authenticate(biometricPromptInfo, cryptoObject) - } else { - biometricPrompt.authenticate(biometricPromptInfo) - } - } - - private fun createMDocDeviceResponse( - credentialId: Int, - dataElements: List, - encodedSessionTranscript: ByteArray, - onComplete: (ByteArray) -> Unit - ) { - val documentRequest = DocumentRequest(dataElements) - val documentStore = ProvisioningUtil.getInstance(applicationContext).documentStore - val documentName = documentStore.listDocuments()[credentialId] - val document = documentStore.lookupDocument(documentName) - val nameSpacedData = document!!.applicationData.getNameSpacedData("documentData") - - val credential = document.findCredential( - ProvisioningUtil.CREDENTIAL_DOMAIN, - Clock.System.now() - ) as MdocCredential? ?: throw IllegalStateException("No credential") - val staticAuthData = StaticAuthDataParser(credential.issuerProvidedData).parse() - val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( - documentRequest, nameSpacedData, staticAuthData - ) - - val deviceResponseGenerator = DeviceResponseGenerator(Constants.DEVICE_RESPONSE_STATUS_OK) - val documentGenerator = DocumentGenerator( - document.applicationData.getString(ProvisioningUtil.DOCUMENT_TYPE), - staticAuthData.issuerAuth, - encodedSessionTranscript - ) - documentGenerator.setIssuerNamespaces(mergedIssuerNamespaces) - try { - addDeviceNamespaces(documentGenerator, credential, null) - deviceResponseGenerator.addDocument(documentGenerator.generate()) - credential.increaseUsageCount() - onComplete(deviceResponseGenerator.generate()) - } catch (e: KeyLockedException) { - doBiometricAuth(credential, false) { keyUnlockData -> - if (keyUnlockData != null) { - addDeviceNamespaces(documentGenerator, credential, keyUnlockData) - deviceResponseGenerator.addDocument(documentGenerator.generate()) - credential.increaseUsageCount() - onComplete(deviceResponseGenerator.generate()) - } else { - throw RuntimeException("Biometric Auth Failed") - } - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - try { - - - val cmrequest = extractGetCredentialRequest(intent) - val credentialId = intent.getLongExtra(EXTRA_CREDENTIAL_ID, -1).toInt() - - // This call is currently broken, have to extract this info manually for now - //val callingAppInfo = extractCallingAppInfo(intent) - val callingPackageName = - intent.getStringExtra(IntentHelper.EXTRA_CALLING_PACKAGE_NAME)!! - val callingOrigin = intent.getStringExtra(IntentHelper.EXTRA_ORIGIN) - - log("CredId: $credentialId ${cmrequest!!.credentialOptions.get(0).requestMatcher}") - log("Calling app $callingPackageName $callingOrigin") - - val dataElements = mutableListOf() - - val json = JSONObject(cmrequest!!.credentialOptions.get(0).requestMatcher) - val provider = json.getJSONArray("providers").getJSONObject(0) - - val protocol = provider.getString("protocol") - log("Request protocol: $protocol") - val request = provider.getString("request") - log("Request: $request") - if (protocol == "preview") { - // Extract params from the preview protocol request - val previewRequest = JSONObject(request) - val selector = previewRequest.getJSONObject("selector") - val nonceBase64 = previewRequest.getString("nonce") - val readerPublicKeyBase64 = previewRequest.getString("readerPublicKey") - val docType = selector.getString("doctype") - log("DocType: $docType") - log("nonce: $nonceBase64") - log("readerPublicKey: $readerPublicKeyBase64") - - // Covert nonce and publicKey - val nonce = Base64.decode(nonceBase64, Base64.NO_WRAP or Base64.URL_SAFE) - val readerPublicKey = EcPublicKeyDoubleCoordinate.fromUncompressedPointEncoding( - EcCurve.P256, - Base64.decode(readerPublicKeyBase64, Base64.NO_WRAP or Base64.URL_SAFE) - ) - - // Match all the requested fields - val fields = selector.getJSONArray("fields") - for (n in 0 until fields.length()) { - val field = fields.getJSONObject(n) - val name = field.getString("name") - val namespace = field.getString("namespace") - val intentToRetain = field.getBoolean("intentToRetain") - log("Field $namespace $name $intentToRetain") - dataElements.add( - DocumentRequest.DataElement( - namespace, - name, - intentToRetain - ) - ) - } - - // Generate the Session Transcript - val encodedSessionTranscript = if (callingOrigin == null) { - CredmanUtil.generateAndroidSessionTranscript( - nonce, - callingPackageName, - Crypto.digest(Algorithm.SHA256, readerPublicKey.asUncompressedPointEncoding) - ) - } else { - CredmanUtil.generateBrowserSessionTranscript( - nonce, - callingOrigin, - Crypto.digest(Algorithm.SHA256, readerPublicKey.asUncompressedPointEncoding) - ) - } - // Create ISO DeviceResponse - createMDocDeviceResponse(credentialId, dataElements, encodedSessionTranscript) { deviceResponse -> - // The Preview protocol HPKE encrypts the response. - val (cipherText, encapsulatedPublicKey) = Crypto.hpkeEncrypt( - Algorithm.HPKE_BASE_P256_SHA256_AES128GCM, - readerPublicKey, - deviceResponse, - encodedSessionTranscript - ) - val encodedCredentialDocument = - CredmanUtil.generateCredentialDocument(cipherText, encapsulatedPublicKey) - - // Create the preview response - val responseJson = JSONObject() - responseJson.put( - "token", - Base64.encodeToString( - encodedCredentialDocument, - Base64.NO_WRAP or Base64.URL_SAFE - ) - ) - val response = responseJson.toString(2) - - // Send result back to credman - val resultData = Intent() - setGetCredentialResponse(resultData, createGetCredentialResponse(response)) - setResult(RESULT_OK, resultData) - finish() - } - } else if (protocol == "openid4vp") { - val openid4vpRequest = JSONObject(request) - val clientID = openid4vpRequest.getString("client_id") - log("client_id $clientID") - val nonceBase64 = openid4vpRequest.getString("nonce") - log("nonce: $nonceBase64") - val nonce = Base64.decode(nonceBase64, Base64.NO_WRAP or Base64.URL_SAFE) - - val presentationDefinition = openid4vpRequest.getJSONObject("presentation_definition") - val inputDescriptors = presentationDefinition.getJSONArray("input_descriptors") - if (inputDescriptors.length() != 1) { - throw IllegalArgumentException("Only support a single input input_descriptor") - } - val inputDescriptor = inputDescriptors.getJSONObject(0)!! - val docType = inputDescriptor.getString("id") - log("DocType: $docType") - - val constraints = inputDescriptor.getJSONObject("constraints") - val fields = constraints.getJSONArray("fields") - - for (n in 0 until fields.length()) { - val field = fields.getJSONObject(n) - // Only support a single path entry for now - val path = field.getJSONArray("path").getString(0)!! - // JSONPath is horrible, hacky way to parse it for demonstration purposes - val st = StringTokenizer(path, "'", false).asSequence().toList() - val namespace = st[1] as String - val name = st[3] as String - log("namespace $namespace name $name") - val intentToRetain = field.getBoolean("intent_to_retain") - dataElements.add( - DocumentRequest.DataElement( - namespace, - name, - intentToRetain - ) - ) - } - // Generate the Session Transcript - val encodedSessionTranscript = if (callingOrigin == null) { - CredmanUtil.generateAndroidSessionTranscript( - nonce, - callingPackageName, - Crypto.digest(Algorithm.SHA256, clientID.toByteArray()) - ) - } else { - CredmanUtil.generateBrowserSessionTranscript( - nonce, - callingOrigin, - Crypto.digest(Algorithm.SHA256, clientID.toByteArray()) - ) - } - // Create ISO DeviceResponse - createMDocDeviceResponse(credentialId, dataElements, encodedSessionTranscript) { deviceResponse -> - // Create the openid4vp respoinse - val responseJson = JSONObject() - responseJson.put( - "vp_token", - Base64.encodeToString( - deviceResponse, - Base64.NO_WRAP or Base64.URL_SAFE - ) - ) - val response = responseJson.toString(2) - - // Send result back to credman - val resultData = Intent() - setGetCredentialResponse(resultData, createGetCredentialResponse(response)) - setResult(RESULT_OK, resultData) - finish() - } - } else { - // Unknown protocol - throw IllegalArgumentException("Unknown protocol") - } - - } catch (e: Exception) { - log("Exception $e") - val resultData = Intent() - setGetCredentialException(resultData, e.toString(), e.message) - setResult(RESULT_OK, resultData) - finish() - } - } - - private fun createGetCredentialResponse(response: String): GetCredentialResponse { - val bundle = Bundle() - bundle.putByteArray("identityToken", response.toByteArray()) - val credentialResponse = com.google.android.gms.identitycredentials.Credential("type", bundle) - return GetCredentialResponse(credentialResponse) - } - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt b/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt deleted file mode 100644 index 5c0ba6e39..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.android.identity.wallet - -import android.app.Application -import android.content.Context -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.storage.AndroidStorageEngine -import com.android.identity.android.util.AndroidLogPrinter -import com.android.identity.credential.CredentialFactory -import com.android.identity.crypto.X509Cert -import com.android.identity.document.DocumentStore -import com.android.identity.documenttype.DocumentTypeRepository -import com.android.identity.documenttype.knowntypes.DrivingLicense -import com.android.identity.documenttype.knowntypes.EUPersonalID -import com.android.identity.documenttype.knowntypes.VaccinationDocument -import com.android.identity.documenttype.knowntypes.VehicleRegistration -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.SecureAreaRepository -import com.android.identity.securearea.software.SoftwareSecureArea -import com.android.identity.storage.GenericStorageEngine -import com.android.identity.storage.StorageEngine -import com.android.identity.trustmanagement.TrustManager -import com.android.identity.trustmanagement.TrustPoint -import com.android.identity.util.Logger -import com.android.identity.wallet.document.KeysAndCertificates -import com.android.identity.wallet.util.PeriodicKeysRefreshWorkRequest -import com.android.identity.wallet.util.PreferencesHelper -import com.google.android.material.color.DynamicColors -import kotlinx.io.files.Path -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.ByteArrayInputStream -import java.security.Security -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate - -class HolderApp: Application() { - - private val documentTypeRepository by lazy { - DocumentTypeRepository() - } - - private val trustManager by lazy { - TrustManager() - } - - private val certificateStorageEngine by lazy { - GenericStorageEngine(Path(getDir("Certificates", MODE_PRIVATE).name)) - } - - override fun onCreate() { - super.onCreate() - Logger.setLogPrinter(AndroidLogPrinter()) - // 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()) - DynamicColors.applyToActivitiesIfAvailable(this) - PreferencesHelper.initialize(this) - PeriodicKeysRefreshWorkRequest(this).schedulePeriodicKeysRefreshing() - documentTypeRepositoryInstance = documentTypeRepository - documentTypeRepositoryInstance.addDocumentType(DrivingLicense.getDocumentType()) - documentTypeRepositoryInstance.addDocumentType(VehicleRegistration.getDocumentType()) - documentTypeRepositoryInstance.addDocumentType(VaccinationDocument.getDocumentType()) - documentTypeRepositoryInstance.addDocumentType(EUPersonalID.getDocumentType()) - trustManagerInstance = trustManager - certificateStorageEngineInstance = certificateStorageEngine - certificateStorageEngineInstance.enumerate().forEach { - val certificate = parseCertificate(certificateStorageEngineInstance.get(it)!!) - trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded))) - } - KeysAndCertificates.getTrustedReaderCertificates(this).forEach { - trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(it.encoded))) - } - } - - companion object { - - lateinit var documentTypeRepositoryInstance: DocumentTypeRepository - lateinit var trustManagerInstance: TrustManager - lateinit var certificateStorageEngineInstance: StorageEngine - fun createDocumentStore( - context: Context, - secureAreaRepository: SecureAreaRepository - ): DocumentStore { - val storageFile = Path(PreferencesHelper.getKeystoreBackedStorageLocation(context).path) - val storageEngine = AndroidStorageEngine.Builder(context, storageFile).build() - - val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine) - val softwareSecureArea = SoftwareSecureArea(storageEngine) - - secureAreaRepository.addImplementation(androidKeystoreSecureArea) - secureAreaRepository.addImplementation(softwareSecureArea) - - var credentialFactory = CredentialFactory() - credentialFactory.addCredentialImplementation(MdocCredential::class) { - document, dataItem -> MdocCredential(document, dataItem) - } - return DocumentStore(storageEngine, secureAreaRepository, credentialFactory) - } - } - - /** - * Parse a byte array as an X509 certificate - */ - private fun parseCertificate(certificateBytes: ByteArray): X509Certificate { - return CertificateFactory.getInstance("X509") - .generateCertificate(ByteArrayInputStream(certificateBytes)) as X509Certificate - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt b/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt deleted file mode 100644 index f4fa6fb12..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.android.identity.wallet - -import android.app.PendingIntent -import android.content.Intent -import android.net.Uri -import android.nfc.NfcAdapter -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.GravityCompat -import androidx.navigation.Navigation -import androidx.navigation.findNavController -import androidx.navigation.ui.NavigationUI -import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController -import androidx.navigation.ui.setupWithNavController -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.mdoc.origininfo.OriginInfoDomain -import com.android.identity.util.Logger -import com.android.identity.wallet.databinding.ActivityMainBinding -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logError -import com.android.identity.wallet.util.logInfo -import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel -import com.google.android.material.elevation.SurfaceColors - -class MainActivity : AppCompatActivity() { - - private val viewModel: ShareDocumentViewModel by viewModels() - private lateinit var binding: ActivityMainBinding - private lateinit var pendingIntent: PendingIntent - private var nfcAdapter: NfcAdapter? = null - - private val navController by lazy { - Navigation.findNavController(this, R.id.nav_host_fragment) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val color = SurfaceColors.SURFACE_2.getColor(this) - window.statusBarColor = color - window.navigationBarColor = color - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - DocumentManager.getInstance(this).registerDocuments() - setupDrawerLayout() - setupNfc() - onNewIntent(intent) - Logger.isDebugEnabled = PreferencesHelper.isDebugLoggingEnabled() - } - - private fun setupNfc() { - nfcAdapter = NfcAdapter.getDefaultAdapter(this) - // Create a generic PendingIntent that will be deliver to this activity. The NFC stack - // will fill in the intent with the details of the discovered tag before delivering to - // this activity. - val intent = Intent(this, javaClass).apply { - addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - } - pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } - - private fun setupDrawerLayout() { - binding.nvSideDrawer.setupWithNavController(navController) - setupActionBarWithNavController(this, navController, binding.dlMainDrawer) - } - - override fun onResume() { - super.onResume() - nfcAdapter?.enableForegroundDispatch(this, pendingIntent, null, null) - } - - override fun onPause() { - super.onPause() - nfcAdapter?.disableForegroundDispatch(this) - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - log("New intent on Activity $intent") - - if (intent == null) { - return - } - - var mdocUri: String? = null - var mdocReferrerUri: String? = null - if (intent.scheme.equals("mdoc")) { - val uri = Uri.parse(intent.toUri(0)) - mdocUri = "mdoc://" + uri.authority - - mdocReferrerUri = intent.extras?.get(Intent.EXTRA_REFERRER)?.toString() - } - - if (mdocUri == null) { - logError("No mdoc:// URI") - return - } - logInfo("uri: $mdocUri") - - val originInfos = ArrayList() - if (mdocReferrerUri == null) { - logWarning("No referrer URI") - // TODO: maybe bail in the future if this isn't set. - } else { - logInfo("referrer: $mdocReferrerUri") - originInfos.add( - OriginInfoDomain( - mdocReferrerUri - ) - ) - } - - viewModel.startPresentationReverseEngagement(mdocUri, originInfos) - val navController = findNavController(R.id.nav_host_fragment) - navController.navigate(R.id.transferDocumentFragment) - } - - override fun onSupportNavigateUp(): Boolean { - return NavigationUI.navigateUp(navController, binding.dlMainDrawer) - } - - override fun onBackPressed() { - if (binding.dlMainDrawer.isDrawerOpen(GravityCompat.START)) { - binding.dlMainDrawer.closeDrawer(GravityCompat.START) - } else { - super.onBackPressed() - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt b/appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt deleted file mode 100644 index 36002d533..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.identity.wallet.adapter - -import android.icu.text.SimpleDateFormat -import android.widget.TextView -import androidx.databinding.BindingAdapter -import java.util.Calendar -import java.util.Locale - -@BindingAdapter("displayDateTime") -fun bindDisplayDateTime(view: TextView, calendar: Calendar?) { - calendar?.let { - val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSXXX", Locale.getDefault()) - view.text = df.format(it.time) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt b/appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt deleted file mode 100644 index d078b798a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * 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.wallet.adapter - - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.DrawableRes -import androidx.navigation.findNavController -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.android.identity.wallet.R -import com.android.identity.wallet.databinding.ListItemDocumentBinding -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.wallet.SelectDocumentFragmentDirections - -/** - * Adapter for the [RecyclerView]. - */ -class DocumentAdapter : ListAdapter(DocumentDiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DocumentViewHolder( - ListItemDocumentBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val document = getItem(position) - (holder as DocumentViewHolder).bind(document) - } - - class DocumentViewHolder( - private val binding: ListItemDocumentBinding - ) : RecyclerView.ViewHolder(binding.root) { - - init { - binding.setClickDetailListener { - binding.document?.let { doc -> - navigateToDetail(doc, it) - } - } - } - - private fun navigateToDetail(document: DocumentInformation, view: View) { - val direction = SelectDocumentFragmentDirections.toDocumentDetail(document.docName) - if (view.findNavController().currentDestination?.id == R.id.wallet) { - view.findNavController().navigate(direction) - } - } - - fun bind(item: DocumentInformation) { - binding.apply { - val cardArt = cardArtFor(item.documentColor) - binding.llItemContainer.setBackgroundResource(cardArt) - document = item - executePendingBindings() - } - } - - @DrawableRes - private fun cardArtFor(cardArt: Int): Int { - return when (cardArt) { - 1 -> R.drawable.yellow_gradient - 2 -> R.drawable.blue_gradient - 3 -> R.drawable.gradient_red - else -> R.drawable.green_gradient - } - } - } -} - -private class DocumentDiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: DocumentInformation, newItem: DocumentInformation): Boolean { - return oldItem.userVisibleName == newItem.userVisibleName - } - - override fun areContentsTheSame(oldItem: DocumentInformation, newItem: DocumentInformation): Boolean { - return oldItem == newItem - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt deleted file mode 100644 index 624f4f692..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.R -import com.android.identity.wallet.support.SecureAreaSupport -import com.android.identity.wallet.theme.HolderAppTheme -import com.android.identity.wallet.transfer.AddDocumentToResponseResult -import com.android.identity.wallet.viewmodel.TransferDocumentViewModel -import com.google.android.material.bottomsheet.BottomSheetDialogFragment - -class AuthConfirmationFragment : BottomSheetDialogFragment() { - - private val viewModel: TransferDocumentViewModel by activityViewModels() - private val arguments by navArgs() - private var isSendingInProgress = mutableStateOf(false) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val elementsToSign = viewModel.requestedElements() - val sheetData = mapToConfirmationSheetData(elementsToSign) - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) - setContent { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxWidth(), - title = getSubtitle(), - isTrustedReader = arguments.readerIsTrusted, - isSendingInProgress = isSendingInProgress.value, - sheetData = sheetData, - onElementToggled = { element -> viewModel.toggleSignedElement(element) }, - onConfirm = { sendResponse() }, - onCancel = { - dismiss() - cancelAuthorization() - } - ) - } - } - } - } - - override fun onCancel(dialog: DialogInterface) { - cancelAuthorization() - } - - private fun cancelAuthorization() { - viewModel.onAuthenticationCancelled() - } - - private fun mapToConfirmationSheetData( - elementsToSign: List - ): List { - return elementsToSign.map { documentData -> - viewModel.addDocumentForSigning(documentData) - val elements = documentData.requestedElements.map { element -> - viewModel.toggleSignedElement(element) - val displayName = stringValueFor( - documentData.requestedDocument.docType, - element.namespace, - element.value - ) - ConfirmationSheetData.DocumentElement(displayName, element) - } - ConfirmationSheetData(documentData.userReadableName, elements) - } - } - - private fun stringValueFor(docType: String, namespace: String, element: String): String { - return HolderApp.documentTypeRepositoryInstance - .getDocumentTypeForMdoc(docType) - ?.mdocDocumentType - ?.namespaces?.get( - namespace - )?.dataElements?.get(element)?.attribute?.displayName ?: element - } - - private fun sendResponse() { - isSendingInProgress.value = true - viewModel.sendResponseForSelection( - onResultReady = { - onSendResponseResult(it) - }) - } - - private fun getSubtitle(): String { - val readerCommonName = arguments.readerCommonName - val readerIsTrusted = arguments.readerIsTrusted - return if (readerCommonName != "") { - if (readerIsTrusted) { - getString(R.string.bio_auth_verifier_trusted_with_name, readerCommonName) - } else { - getString(R.string.bio_auth_verifier_untrusted_with_name, readerCommonName) - } - } else { - getString(R.string.bio_auth_verifier_anonymous) - } - } - - private fun onSendResponseResult(result: AddDocumentToResponseResult) { - when (result) { - is AddDocumentToResponseResult.DocumentLocked -> { - - val secureAreaSupport = SecureAreaSupport.getInstance( - requireContext(), - result.credential.secureArea - ) - with(secureAreaSupport) { - unlockKey( - credential = result.credential, - onKeyUnlocked = { keyUnlockData -> - viewModel.sendResponseForSelection( - onResultReady = { - onSendResponseResult(it) - }, - result.credential, - keyUnlockData - ) - }, - onUnlockFailure = { wasCancelled -> - if (wasCancelled) { - cancelAuthorization() - } else { - viewModel.closeConnection() - } - } - ) - } - } - - is AddDocumentToResponseResult.DocumentAdded -> { - if (result.signingKeyUsageLimitPassed) { - toast("Using previously used Auth Key") - } - findNavController().navigateUp() - } - } - } - - private fun toast(message: String, duration: Int = Toast.LENGTH_SHORT) { - Toast.makeText(requireContext(), message, duration).show() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt deleted file mode 100644 index f0912438e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt +++ /dev/null @@ -1,370 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -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.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Done -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R -import com.android.identity.wallet.authconfirmation.ConfirmationSheetData.DocumentElement -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun ConfirmationSheet( - modifier: Modifier = Modifier, - title: String, - isTrustedReader: Boolean = false, - isSendingInProgress: Boolean = false, - sheetData: List = emptyList(), - onElementToggled: (element: RequestedElement) -> Unit = { }, - onConfirm: () -> Unit = {}, - onCancel: () -> Unit = {} -) { - Column(modifier = modifier) { - BottomSheetHandle( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) - if (isTrustedReader) { - TrustedReaderCheck( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) - } - Text( - text = title, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.4f) - ) { - DocumentElements(sheetData, onElementToggled) - if (isSendingInProgress) { - LoadingIndicator( - modifier = Modifier - .matchParentSize() - .padding(horizontal = 8.dp) - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)), - ) - } - } - SheetActions( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - enabled = !isSendingInProgress, - onCancel = onCancel, - onConfirm = onConfirm - ) - } -} - -@Composable -private fun BottomSheetHandle( - modifier: Modifier = Modifier, -) { - Row(modifier = modifier, horizontalArrangement = Arrangement.Center) { - Spacer( - modifier = Modifier - .size(64.dp, 4.dp) - .clip(RoundedCornerShape(4.dp)) - .background(Color.Gray) - ) - } -} - -@Composable -private fun TrustedReaderCheck( - modifier: Modifier = Modifier, -) { - Row(modifier = modifier, horizontalArrangement = Arrangement.Center) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) - .size(48.dp), - contentAlignment = Alignment.Center - ) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.Default.Check, - contentDescription = "", - tint = MaterialTheme.colorScheme.primary - ) - } - } -} - -@Composable -private fun DocumentTitle( - modifier: Modifier = Modifier, - document: ConfirmationSheetData -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text( - textAlign = TextAlign.Center, - text = document.documentName, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) - ) - } -} - -@Composable -private fun ChipsRow( - modifier: Modifier = Modifier, - left: DocumentElement, - right: DocumentElement?, - onElementToggled: (element: RequestedElement) -> Unit -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - val chipModifier = if (right != null) Modifier.weight(1f) else Modifier - ElementChip( - modifier = chipModifier, - documentElement = left, - onElementToggled = onElementToggled - ) - right?.let { - Spacer(modifier = Modifier.width(8.dp)) - ElementChip( - modifier = chipModifier, - documentElement = right, - onElementToggled = onElementToggled - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ElementChip( - modifier: Modifier = Modifier, - documentElement: DocumentElement, - onElementToggled: (element: RequestedElement) -> Unit -) { - var isChecked by remember { mutableStateOf(true) } - FilterChip( - modifier = modifier, - selected = isChecked, - onClick = { - isChecked = !isChecked - onElementToggled(documentElement.requestedElement) - }, - label = { Text(text = documentElement.displayName) }, - leadingIcon = { - AnimatedVisibility(visible = isChecked) { - Icon( - imageVector = Icons.Filled.Done, - contentDescription = "", - modifier = Modifier.size(FilterChipDefaults.IconSize) - ) - } - } - ) -} - -@Composable -@OptIn(ExperimentalFoundationApi::class) -private fun DocumentElements( - sheetData: List, - onElementToggled: (element: RequestedElement) -> Unit -) { - LazyColumn(modifier = Modifier.focusGroup()) { - sheetData.forEach { document -> - stickyHeader { - DocumentTitle( - modifier = Modifier - .fillMaxWidth() - .height(42.dp) - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant), - document = document - ) - } - val grouped = document.elements.chunked(2).map { pair -> - if (pair.size == 1) Pair(pair.first(), null) - else Pair(pair.first(), pair.last()) - } - items(grouped.size) { index -> - val items = grouped[index] - ChipsRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - left = items.first, - right = items.second, - onElementToggled = onElementToggled - ) - } - } - } -} - -@Composable -private fun LoadingIndicator( - modifier: Modifier = Modifier -) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } -} - -@Composable -private fun SheetActions( - modifier: Modifier = Modifier, - enabled: Boolean, - onCancel: () -> Unit, - onConfirm: () -> Unit -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Button( - modifier = Modifier.weight(1f), - enabled = enabled, - onClick = { - if (enabled) { - onCancel() - } - } - ) { - Text(text = stringResource(id = R.string.bt_cancel)) - } - Spacer(modifier = Modifier.width(8.dp)) - Button( - modifier = Modifier.weight(1f), - enabled = enabled, - onClick = { - if (enabled) { - onConfirm() - } - } - ) { - Text(text = stringResource(id = R.string.btn_send_data)) - } - } -} - -@Composable -@Preview(name = "Default", showBackground = true) -@Preview(name = "Default", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSheet() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Title" - ) - } -} - -@Composable -@Preview(name = "Default With Trusted Reader", showBackground = true) -@Preview(name = "Default With Trusted Reader", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSheetTrustedReader() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Title", - isTrustedReader = true - ) - } -} - -@Composable -@Preview(name = "Document With Trusted Reader", showBackground = true) -@Preview(name = "Document With Trusted Reader", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSheetWithDocumentAndTrustedReader() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Trusted verifier 'Google' is requesting the following information", - isTrustedReader = true, - sheetData = listOf( - ConfirmationSheetData( - documentName = "Driving Licence | mDL", - elements = (1..11).map { DocumentElement("Property $it", RequestedElement("$it", "namespace")) } - ) - ) - ) - } -} - -@Composable -@Preview(name = "Sending progress", showBackground = true) -@Preview(name = "Sending progress", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSendingProgress() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Trusted verifier 'Google' is requesting the following information", - isSendingInProgress = true, - sheetData = listOf( - ConfirmationSheetData( - documentName = "Driving Licence | mDL", - elements = (1..11).map { DocumentElement("Property $it", RequestedElement("$it", "namespace")) } - ) - ) - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt deleted file mode 100644 index 3031c002c..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable - -@Stable -@Immutable -data class ConfirmationSheetData( - val documentName: String, - val elements: List -) { - - data class DocumentElement( - val displayName: String, - val requestedElement: RequestedElement - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt deleted file mode 100644 index 1488a1108..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -sealed class PassphraseAuthResult { - object Idle: PassphraseAuthResult() - data class Success(val userPassphrase: String): PassphraseAuthResult() -} diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt deleted file mode 100644 index d4d09f2bb..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.PreviewLightDark -import com.android.identity.wallet.theme.HolderAppTheme - -class PassphrasePrompt : DialogFragment() { - - private val args by navArgs() - private val viewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - PassphrasePromptUI( - showIncorrectPassword = args.showIncorrectPassword, - onDone = { passphrase -> - viewModel.authorize(userPassphrase = passphrase) - findNavController().navigateUp() - } - ) - } - } - } - } -} - -@Composable -private fun PassphrasePromptUI( - showIncorrectPassword: Boolean, - onDone: (passphrase: String) -> Unit -) { - var value by remember { mutableStateOf("") } - Card( - modifier = Modifier.fillMaxWidth(), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = stringResource(id = R.string.passphrase_prompt_title), - style = MaterialTheme.typography.titleLarge - ) - Text( - text = stringResource(id = R.string.passphrase_prompt_message), - style = MaterialTheme.typography.titleSmall - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = value, - onValueChange = { value = it }, - textStyle = MaterialTheme.typography.bodyMedium, - visualTransformation = PasswordVisualTransformation(), - placeholder = { - Text( - text = stringResource(id = R.string.passphrase_prompt_hint), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) - ) - } - ) - if (showIncorrectPassword) { - Text( - text = stringResource(id = R.string.passphrase_prompt_incorrect_passphrase), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.error - ) - } - TextButton( - modifier = Modifier - .align(Alignment.End), - onClick = { onDone(value) }) { - Text(text = stringResource(id = R.string.bt_ok)) - } - } - } -} - -@Composable -@PreviewLightDark -private fun PreviewPassphrasePrompt() { - HolderAppTheme { - PassphrasePromptUI( - showIncorrectPassword = false, - onDone = {} - ) - } -} - -@Composable -@PreviewLightDark -private fun PreviewPassphrasePromptWithIncorrectPassword() { - HolderAppTheme { - PassphrasePromptUI( - showIncorrectPassword = true, - onDone = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt deleted file mode 100644 index 03a477134..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -class PassphrasePromptViewModel : ViewModel() { - - private val _authorizationState = - MutableStateFlow(PassphraseAuthResult.Idle) - val authorizationState: StateFlow = _authorizationState - - fun authorize(userPassphrase: String) { - _authorizationState.update { PassphraseAuthResult.Success(userPassphrase) } - } - - fun reset() { - _authorizationState.update { PassphraseAuthResult.Idle } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt deleted file mode 100644 index dd0dd15f4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import com.android.identity.mdoc.request.DeviceRequestParser - -data class RequestedDocumentData( - val userReadableName: String, - val identityCredentialName: String, - val requestedElements: ArrayList, - val requestedDocument: DeviceRequestParser.DocRequest -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt deleted file mode 100644 index e0e444cc9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -data class RequestedElement( - val namespace: String, - val value: String -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt deleted file mode 100644 index 786d95304..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -class SignedDocumentData( - private val signedElements: List, - val identityCredentialName: String, - val documentType: String, -) { - - fun issuerSignedEntries(): MutableMap> { - val byNamespace = signedElements.groupBy { it.namespace } - val result = mutableMapOf>() - byNamespace.forEach { (namespace, elements) -> - result[namespace] = elements.map { it.value } - } - return result - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt deleted file mode 100644 index 91bc2d80f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -class SignedElementsCollection { - - private val requestedDocuments = mutableMapOf() - private val signedElements = mutableListOf() - - fun addNamespace(requestedData: RequestedDocumentData) { - this.requestedDocuments[requestedData.identityCredentialName] = requestedData - } - - fun toggleProperty(element: RequestedElement) { - if (!signedElements.remove(element)) { - signedElements.add(element) - } - } - - fun collect(): List { - return requestedDocuments.keys.map { namespace -> - val document = requestedDocuments.getValue(namespace) - SignedDocumentData( - signedElements = signedElements, - identityCredentialName = document.identityCredentialName, - documentType = document.requestedDocument.docType, - ) - } - } - - fun clear() { - requestedDocuments.clear() - signedElements.clear() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt b/appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt deleted file mode 100644 index 8a0d6ea83..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.authprompt - -import androidx.biometric.BiometricPrompt - -class BiometricUserAuthPrompt( - private val prompt: BiometricPrompt, - private val promptInfo: BiometricPrompt.PromptInfo -) { - - fun authenticate(cryptoObject: BiometricPrompt.CryptoObject?) { - if (cryptoObject != null) { - prompt.authenticate(promptInfo, cryptoObject) - } else { - prompt.authenticate(promptInfo) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt b/appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt deleted file mode 100644 index 10a3fcdd0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.android.identity.wallet.authprompt - -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS -import androidx.biometric.BiometricPrompt -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import com.android.identity.wallet.util.log - -class UserAuthPromptBuilder private constructor(private val fragment: Fragment) { - - private var title: String = "" - private var subtitle: String = "" - private var description: String = "" - private var negativeButton: String = "" - private var forceLskf: Boolean = false - private var onSuccess: () -> Unit = {} - private var onFailure: () -> Unit = {} - private var onCancelled: () -> Unit = {} - - private val biometricAuthCallback = object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - // reached max attempts to authenticate the user, or authentication dialog was cancelled - if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { - onCancelled.invoke() - } else { - log("User authentication failed $errorCode - $errString") - onFailure.invoke() - } - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - log("User authentication succeeded") - onSuccess.invoke() - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - log("User authentication failed") - onFailure.invoke() - } - } - - fun withTitle(title: String) = apply { - this.title = title - } - - fun withSubtitle(subtitle: String) = apply { - this.subtitle = subtitle - } - - fun withDescription(description: String) = apply { - this.description = description - } - - fun withNegativeButton(negativeButton: String) = apply { - this.negativeButton = negativeButton - } - - fun setForceLskf(forceLskf: Boolean) = apply { - this.forceLskf = forceLskf - } - - fun withSuccessCallback(onSuccess: () -> Unit) = apply { - this.onSuccess = onSuccess - } - - fun withFailureCallback(onFailure: () -> Unit) = apply { - this.onFailure = onFailure - } - - fun withCancelledCallback(onCancelled: () -> Unit) = apply { - this.onCancelled = onCancelled - } - - fun build(): BiometricUserAuthPrompt { - val promptInfoBuilder = BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setSubtitle(subtitle) - .setDescription(description) - .setConfirmationRequired(false) - - 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 PIN" button. Longer term, we should - // fall back to using KeyGuard which will work on all Android versions. - promptInfoBuilder.setAllowedAuthenticators(DEVICE_CREDENTIAL) - } else { - val canUseBiometricAuth = BiometricManager - .from(fragment.requireContext()) - .canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS - if (canUseBiometricAuth) { - promptInfoBuilder.setNegativeButtonText(negativeButton) - } else { - // No biometrics enrolled, force use of LSKF - promptInfoBuilder.setDeviceCredentialAllowed(true) - } - } - - val promptInfo = promptInfoBuilder.build() - val executor = ContextCompat.getMainExecutor(fragment.requireContext()) - val prompt = BiometricPrompt(fragment, executor, biometricAuthCallback) - return BiometricUserAuthPrompt(prompt, promptInfo) - } - - companion object { - fun requestUserAuth(fragment: Fragment): UserAuthPromptBuilder { - return UserAuthPromptBuilder(fragment) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt deleted file mode 100644 index 45473e5af..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Checkbox -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.AuthTypeState -import com.android.identity.wallet.selfsigned.OutlinedContainerVertical - -@Composable -fun AndroidSetupContainer( - modifier: Modifier = Modifier, - isOn: Boolean, - timeoutSeconds: Int, - lskfAuthTypeState: AuthTypeState, - biometricAuthTypeState: AuthTypeState, - useStrongBox: AuthTypeState, - onUserAuthenticationChanged: (isOn: Boolean) -> Unit, - onAuthTimeoutChanged: (authTimeout: Int) -> Unit, - onLskfAuthChanged: (isOn: Boolean) -> Unit, - onBiometricAuthChanged: (isOn: Boolean) -> Unit, - onStrongBoxChanged: (isOn: Boolean) -> Unit -) { - Column(modifier = modifier) { - OutlinedContainerVertical(modifier = Modifier.fillMaxWidth()) { - val labelOn = stringResource(id = R.string.user_authentication_on) - val labelOff = stringResource(id = R.string.user_authentication_off) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = if (isOn) labelOn else labelOff, - ) - Switch( - modifier = Modifier.padding(start = 8.dp), - checked = isOn, - onCheckedChange = onUserAuthenticationChanged - ) - } - AnimatedVisibility( - modifier = Modifier.fillMaxWidth(), - visible = isOn - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = stringResource(id = R.string.keystore_android_user_auth_timeout) - ) - NumberChanger( - number = timeoutSeconds, - onNumberChanged = onAuthTimeoutChanged, - counterTextStyle = MaterialTheme.typography.titleLarge - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val alpha = if (lskfAuthTypeState.canBeModified) 1f else .5f - ValueLabel( - modifier = Modifier - .weight(1f) - .alpha(alpha), - label = stringResource(id = R.string.user_auth_type_allow_lskf) - ) - Checkbox( - checked = lskfAuthTypeState.isEnabled, - onCheckedChange = onLskfAuthChanged, - enabled = lskfAuthTypeState.canBeModified - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val alpha = if (biometricAuthTypeState.canBeModified) 1f else .5f - ValueLabel( - modifier = Modifier - .weight(1f) - .alpha(alpha), - label = stringResource(id = R.string.user_auth_type_allow_biometric) - ) - Checkbox( - checked = biometricAuthTypeState.isEnabled, - onCheckedChange = onBiometricAuthChanged, - enabled = biometricAuthTypeState.canBeModified - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val alpha = if (useStrongBox.canBeModified) 1f else .5f - ValueLabel( - modifier = Modifier - .weight(1f) - .alpha(alpha), - label = stringResource(id = R.string.user_auth_use_strong_box) - ) - Checkbox( - checked = useStrongBox.isEnabled, - onCheckedChange = onStrongBoxChanged, - enabled = useStrongBox.canBeModified - ) - } - } - } - } - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt deleted file mode 100644 index 0bed3a7c6..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.MdocAuthOption -import com.android.identity.wallet.composables.state.MdocAuthStateOption -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOption -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState - -@Composable -fun AuthenticationKeyCurveAndroid( - modifier: Modifier = Modifier, - state: AndroidAuthKeyCurveState, - mDocAuthState: MdocAuthOption, - onAndroidAuthKeyCurveChanged: (newValue: AndroidAuthKeyCurveOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.authentication_key_curve_label) - ) { - var keyCurveDropDownExpanded by remember { mutableStateOf(false) } - val clickModifier = if (state.isEnabled) { - Modifier.clickable { keyCurveDropDownExpanded = true } - } else { - Modifier - } - val alpha = if (state.isEnabled) 1f else .5f - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = curveLabelFor(state.authCurve.toEcCurve()) - ) - DropDownIndicator() - } - DropdownMenu( - expanded = keyCurveDropDownExpanded, - onDismissRequest = { keyCurveDropDownExpanded = false } - ) { - val ecCurveOption = - if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { - AndroidAuthKeyCurveOption.Ed25519 - } else { - AndroidAuthKeyCurveOption.X25519 - } - TextDropDownRow( - label = curveLabelFor(curveOption = AndroidAuthKeyCurveOption.P_256.toEcCurve()), - onSelected = { - onAndroidAuthKeyCurveChanged(AndroidAuthKeyCurveOption.P_256) - keyCurveDropDownExpanded = false - } - ) - TextDropDownRow( - label = curveLabelFor(curveOption = ecCurveOption.toEcCurve()), - onSelected = { - onAndroidAuthKeyCurveChanged(ecCurveOption) - keyCurveDropDownExpanded = false - } - ) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt deleted file mode 100644 index 5f9f94be5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveOption -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveState -import com.android.identity.wallet.composables.state.MdocAuthOption -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -@Composable -fun AuthenticationKeyCurveSoftware( - modifier: Modifier = Modifier, - state: SoftwareAuthKeyCurveState, - mDocAuthState: MdocAuthOption, - onSoftwareAuthKeyCurveChanged: (newValue: SoftwareAuthKeyCurveOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.authentication_key_curve_label) - ) { - var keyCurveDropDownExpanded by remember { mutableStateOf(false) } - val clickModifier = if (state.isEnabled) { - Modifier.clickable { keyCurveDropDownExpanded = true } - } else { - Modifier - } - val alpha = if (state.isEnabled) 1f else .5f - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = curveLabelFor(state.authCurve.toEcCurve()) - ) - DropDownIndicator() - } - val entries = - SoftwareAuthKeyCurveOption.values().toMutableList() - if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { - entries.remove(SoftwareAuthKeyCurveOption.X448) - entries.remove(SoftwareAuthKeyCurveOption.X25519) - } else { - entries.remove(SoftwareAuthKeyCurveOption.Ed448) - entries.remove(SoftwareAuthKeyCurveOption.Ed25519) - } - DropdownMenu( - expanded = keyCurveDropDownExpanded, - onDismissRequest = { keyCurveDropDownExpanded = false } - ) { - for (entry in entries) { - TextDropDownRow( - label = curveLabelFor(curveOption = entry.toEcCurve()), - onSelected = { - onSoftwareAuthKeyCurveChanged(entry) - keyCurveDropDownExpanded = false - } - ) - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt deleted file mode 100644 index 4b5e48695..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun CounterInput( - modifier: Modifier = Modifier, - label: String, - value: Int, - onValueChange: (newValue: Int) -> Unit -) { - Column(modifier = modifier) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - ValueLabel( - modifier = Modifier.weight(1f), - label = label - ) - NumberChanger(number = value, onNumberChanged = onValueChange) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt b/appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt deleted file mode 100644 index 3f9a9e047..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.android.identity.crypto.EcCurve -import com.android.identity.wallet.R - -@Composable -fun curveLabelFor( - curveOption: EcCurve -): String { - return when (curveOption) { - EcCurve.P256 -> stringResource(id = R.string.curve_p_256) - EcCurve.P384 -> stringResource(id = R.string.curve_p_384) - EcCurve.P521 -> stringResource(id = R.string.curve_p_521) - EcCurve.BRAINPOOLP256R1 -> stringResource(id = R.string.curve_brain_pool_p_256R1) - EcCurve.BRAINPOOLP320R1 -> stringResource(id = R.string.curve_brain_pool_p_320R1) - EcCurve.BRAINPOOLP384R1 -> stringResource(id = R.string.curve_brain_pool_p_384R1) - EcCurve.BRAINPOOLP512R1 -> stringResource(id = R.string.curve_brain_pool_p_512R1) - EcCurve.ED25519 -> stringResource(id = R.string.curve_ed25519) - EcCurve.X25519 -> stringResource(id = R.string.curve_x25519) - EcCurve.ED448 -> stringResource(id = R.string.curve_ed448) - EcCurve.X448 -> stringResource(id = R.string.curve_X448) - else -> "" - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt b/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt deleted file mode 100644 index 82066891a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.ui.graphics.Brush -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.theme.BlueGradient -import com.android.identity.wallet.theme.GreenGradient -import com.android.identity.wallet.theme.RedGradient -import com.android.identity.wallet.theme.YellowGradient - -fun Int.toCardArt(): DocumentColor { - return when (this) { - 1 -> DocumentColor.Yellow - 2 -> DocumentColor.Blue - 3 -> DocumentColor.Red - else -> DocumentColor.Green - } -} - -fun gradientFor(cardArt: DocumentColor): Brush { - return when (cardArt) { - is DocumentColor.Green -> GreenGradient - is DocumentColor.Yellow -> YellowGradient - is DocumentColor.Blue -> BlueGradient - is DocumentColor.Red -> RedGradient - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt b/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt deleted file mode 100644 index 06f09b8fe..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun DropDownIndicator( - modifier: Modifier = Modifier -) { - Icon( - modifier = modifier, - imageVector = Icons.Default.ArrowDropDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt deleted file mode 100644 index b642101d7..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun LabeledUserInput( - modifier: Modifier = Modifier, - label: String, - content: @Composable () -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row(modifier = Modifier.fillMaxWidth()) { - ValueLabel(label = label) - } - content() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt b/appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt deleted file mode 100644 index 3732a1217..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun LoadingIndicator( - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .background(MaterialTheme.colorScheme.surface.copy(alpha = .5f)), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } -} - -@Preview -@Composable -private fun PreviewLoadingIndicator() { - HolderAppTheme { - LoadingIndicator(modifier = Modifier.fillMaxSize()) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt b/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt deleted file mode 100644 index b52cd286a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.MdocAuthOption -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -@Composable -fun MdocAuthentication( - modifier: Modifier = Modifier, - state: MdocAuthOption, - onMdocAuthOptionChange: (newValue: MdocAuthStateOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.mdoc_authentication_label) - ) { - var expanded by remember { mutableStateOf(false) } - val alpha = if (state.isEnabled) 1f else .5f - val clickModifier = if (state.isEnabled) { - Modifier.clickable { expanded = true } - } else { - Modifier - } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = mdocAuthOptionLabelFor(state.mDocAuthentication) - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier.fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - TextDropDownRow( - label = stringResource(id = R.string.mdoc_auth_ecdsa), - onSelected = { - onMdocAuthOptionChange(MdocAuthStateOption.ECDSA) - expanded = false - } - ) - TextDropDownRow( - label = stringResource(id = R.string.mdoc_auth_mac), - onSelected = { - onMdocAuthOptionChange(MdocAuthStateOption.MAC) - expanded = false - } - ) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt b/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt deleted file mode 100644 index e9e4ea904..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.with -import androidx.compose.foundation.layout.Row -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun NumberChanger( - modifier: Modifier = Modifier, - number: Int, - onNumberChanged: (newValue: Int) -> Unit, - counterTextStyle: TextStyle = MaterialTheme.typography.bodyLarge -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { onNumberChanged(number - 1) }) { - Icon(imageVector = Icons.Default.Remove, contentDescription = null) - } - AnimatedContent( - targetState = number, - label = "", - transitionSpec = { - if (targetState > initialState) { - slideInVertically { -it } with slideOutVertically { it } - } else { - slideInVertically { it } with slideOutVertically { -it } - } - } - ) { count -> - Text( - text = "$count", - textAlign = TextAlign.Center, - style = counterTextStyle - ) - } - IconButton(onClick = { onNumberChanged(number + 1) }) { - Icon(imageVector = Icons.Default.Add, contentDescription = null) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt b/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt deleted file mode 100644 index ceae2ab58..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -@Composable -fun OutlinedContainerHorizontal( - modifier: Modifier = Modifier, - outlineBorderWidth: Dp = 2.dp, - outlineBrush: Brush? = null, - content: @Composable RowScope.() -> Unit -) { - val brush = outlineBrush ?: SolidColor(MaterialTheme.colorScheme.outline) - Row( - modifier = modifier - .heightIn(48.dp) - .clip(RoundedCornerShape(12.dp)) - .border(outlineBorderWidth, brush, RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.inverseOnSurface), - verticalAlignment = Alignment.CenterVertically - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - content() - } - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt b/appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt deleted file mode 100644 index d5a9167b3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.identity.wallet.composables - -import android.content.res.Configuration -import androidx.compose.ui.tooling.preview.Preview - -@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) -annotation class PreviewLightDark \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt deleted file mode 100644 index 914da5c1e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R - -@Composable -fun SoftwareSetupContainer( - modifier: Modifier = Modifier, - passphrase: String, - onPassphraseChanged: (newValue: String) -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - Box(contentAlignment = Alignment.CenterStart) { - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - textStyle = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - value = passphrase, - onValueChange = onPassphraseChanged, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) - ) - if (passphrase.isEmpty()) { - Text( - text = stringResource(id = R.string.keystore_software_passphrase_hint), - style = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = .5f) - ), - ) - } - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt b/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt deleted file mode 100644 index e9c9dc1d3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun TextDropDownRow( - modifier: Modifier = Modifier, - label: String, - onSelected: () -> Unit -) { - DropdownMenuItem( - modifier = modifier, - text = { - ValueLabel( - modifier = Modifier.fillMaxWidth(), - label = label - ) - }, - onClick = onSelected - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt b/appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt deleted file mode 100644 index 1b920eba8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.composables - -import android.widget.Toast -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -@Composable -fun ShowToast(message: String, duration: Int = Toast.LENGTH_SHORT) { - Toast.makeText(LocalContext.current, message, duration).show() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt b/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt deleted file mode 100644 index 2d1ff3c65..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun ValueLabel( - modifier: Modifier = Modifier, - label: String -) { - Text( - modifier = modifier, - text = label, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelMedium - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt b/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt deleted file mode 100644 index f86eaec29..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -@Composable -fun mdocAuthOptionLabelFor( - state: MdocAuthStateOption -): String { - return when (state) { - MdocAuthStateOption.ECDSA -> - stringResource(id = R.string.mdoc_auth_ecdsa) - - MdocAuthStateOption.MAC -> - stringResource(id = R.string.mdoc_auth_mac) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt deleted file mode 100644 index 67236282b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.composables.state - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AuthTypeState( - val isEnabled: Boolean = true, - val canBeModified: Boolean = false -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt deleted file mode 100644 index 06c424f66..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.composables.state - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MdocAuthOption( - val isEnabled: Boolean = true, - val mDocAuthentication: MdocAuthStateOption = MdocAuthStateOption.ECDSA -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt deleted file mode 100644 index eb6a7923b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.android.identity.wallet.composables.state - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class MdocAuthStateOption : Parcelable { - ECDSA, MAC -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt b/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt deleted file mode 100644 index 36cda199e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.android.mdl.app.credman - -import android.graphics.Bitmap -import java.io.ByteArrayOutputStream -import org.json.JSONArray -import org.json.JSONObject - -class IdentityCredentialEntry( - val id: Long, - val format: String, - val title: String, - val subtitle: String, - val icon: Bitmap, - val fields: List, - val disclaimer: String?, - val warning: String?, -) { - fun getIconBytes(): ByteArrayOutputStream { - val scaledIcon = Bitmap.createScaledBitmap(icon, 128, 128, true) - val stream = ByteArrayOutputStream() - scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, stream) - return stream - } - - fun toJson(iconIndex: Int?): JSONObject { - val credential = JSONObject() - credential.put("format", format) - val displayInfo = JSONObject() - displayInfo.put("title", title) - displayInfo.putOpt("subtitle", subtitle) - displayInfo.putOpt("disclaimer", disclaimer) - displayInfo.putOpt("warning", warning) - displayInfo.putOpt("icon_id", iconIndex) - credential.put("display_info", displayInfo) - val fieldsJson = JSONArray() - fields.forEach { fieldsJson.put(it.toJson()) } - credential.put("fields", fieldsJson) - - val result = JSONObject() - result.put("id", id) - result.put("credential", credential) - return result - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt b/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt deleted file mode 100644 index b3ffd1c05..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.mdl.app.credman - -import org.json.JSONObject - -class IdentityCredentialField( - val name: String, - val value: Any?, - val displayName: String, - val displayValue: String?, -) { - fun toJson(): JSONObject { - val field = JSONObject() - field.put("name", name) - field.putOpt("value", value) - field.put("display_name", displayName) - field.putOpt("display_value", displayValue) - return field - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt b/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt deleted file mode 100644 index 44025da28..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.android.mdl.app.credman - -import android.content.Context -import com.google.android.gms.identitycredentials.RegistrationRequest -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -import java.nio.ByteOrder -import org.json.JSONArray -import org.json.JSONObject - -class IdentityCredentialRegistry( - val entries: List, -) { - fun toRegistrationRequest(context: Context): RegistrationRequest { - - return RegistrationRequest( - credentials = credentialBytes(), - matcher = loadMatcher(context), - type = "com.credman.IdentityCredential", - requestType = "", - protocolTypes = emptyList(), - ) - } - - private fun loadMatcher(context: Context): ByteArray { - val stream = context.assets.open("identitycredentialmatcher.wasm"); - val matcher = ByteArray(stream.available()) - stream.read(matcher) - stream.close() - return matcher - } - - private fun credentialBytes(): ByteArray { - val json = JSONObject() - val credListJson = JSONArray() - val icons = ByteArrayOutputStream() - val iconSizeList = mutableListOf() - entries.forEach { entry -> - val iconBytes = entry.getIconBytes() - credListJson.put(entry.toJson(iconSizeList.size)) - iconSizeList.add(iconBytes.size()) - iconBytes.writeTo(icons) - } - json.put("credentials", credListJson) - val credsBytes = json.toString(0).toByteArray() - val result = ByteArrayOutputStream() - // header_size - result.write(intBytes((3 + iconSizeList.size) * Int.SIZE_BYTES)) - // creds_size - result.write(intBytes(credsBytes.size)) - // icon_size_array_size - result.write(intBytes(iconSizeList.size)) - // icon offsets - iconSizeList.forEach { result.write(intBytes(it)) } - result.write(credsBytes) - icons.writeTo(result) - return result.toByteArray() - } - - companion object { - fun intBytes(num: Int): ByteArray = - ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(num).array() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt deleted file mode 100644 index dde802c79..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.identity.wallet.document - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -sealed class DocumentColor(val value: Int) : Parcelable { - object Green : DocumentColor(0) - object Yellow : DocumentColor(1) - object Blue : DocumentColor(2) - object Red : DocumentColor(3) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt deleted file mode 100644 index b78174e00..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.android.identity.wallet.document - -import com.android.identity.crypto.EcCurve -import com.android.identity.securearea.KeyPurpose - -data class DocumentInformation( - val userVisibleName: String, - val docName: String, - val docType: String, - val dateProvisioned: String, - val selfSigned: Boolean, - val documentColor: Int, - val maxUsagesPerKey: Int, - val lastTimeUsed: String, - val authKeys: List -) { - - data class KeyData( - val counter: Int, - val validFrom: String, - val validUntil: String, - val domain: String, - val issuerDataBytesCount: Int, - val usagesCount: Int, - val keyPurposes: KeyPurpose, - val ecCurve: EcCurve, - val isHardwareBacked: Boolean, - val secureAreaDisplayName: String - ) -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt deleted file mode 100644 index 829b9c0b7..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt +++ /dev/null @@ -1,350 +0,0 @@ -package com.android.identity.wallet.document - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import co.nstant.`in`.cbor.CborBuilder -import co.nstant.`in`.cbor.model.DataItem -import co.nstant.`in`.cbor.model.UnicodeString -import com.android.identity.cbor.Cbor -import com.android.identity.document.Document -import com.android.identity.document.NameSpacedData -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.wallet.selfsigned.SelfSignedDocumentData -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.util.FormatUtil -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.util.ProvisioningUtil -import com.android.identity.wallet.util.ProvisioningUtil.Companion.toDocumentInformation -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logError -import com.android.identity.wallet.R -import com.android.mdl.app.credman.IdentityCredentialEntry -import com.android.mdl.app.credman.IdentityCredentialField -import com.android.mdl.app.credman.IdentityCredentialRegistry -import com.google.android.gms.identitycredentials.IdentityCredentialManager -import java.io.ByteArrayOutputStream -import java.util.Locale - -class DocumentManager private constructor(private val context: Context) { - val client = IdentityCredentialManager.Companion.getClient(context) - companion object { - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: DocumentManager? = null - - fun getInstance(context: Context) = - instance ?: synchronized(this) { - instance ?: DocumentManager(context).also { instance = it } - } - } - - fun getDocumentInformation(documentName: String): DocumentInformation? { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - val document = documentStore.lookupDocument(documentName) - return document.toDocumentInformation() - } - - fun getDocumentByName(documentName: String): Document? { - val documentInfo = getDocumentInformation(documentName) - documentInfo?.let { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - return documentStore.lookupDocument(documentName) - } - return null - } - - fun getDataElementDisplayName(docTypeName : String, - nameSpaceName : String, - dataElementName : String): String { - val credType = HolderApp.documentTypeRepositoryInstance.getDocumentTypeForMdoc(docTypeName) - if (credType != null) { - val mdocDataElement = credType.mdocDocumentType!! - .namespaces[nameSpaceName]?.dataElements?.get(dataElementName) - if (mdocDataElement != null) { - return mdocDataElement.attribute.displayName - } - } - return dataElementName - } - - fun registerDocuments() { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - var idCount = 0L - val entries = documentStore.listDocuments().map { documentId -> - val document = documentStore.lookupDocument(documentId)!! - val documentInformation = document.toDocumentInformation()!! - - val fields = mutableListOf() - fields.add(IdentityCredentialField( - name = "doctype", - value = documentInformation.docType, - displayName = "Document Type", - displayValue = documentInformation.docType - )) - - val nameSpacedData = document.applicationData.getNameSpacedData("documentData") - nameSpacedData.nameSpaceNames.map {nameSpaceName -> - nameSpacedData.getDataElementNames(nameSpaceName).map {dataElementName -> - val fieldName = nameSpaceName + "." + dataElementName - val valueCbor = nameSpacedData.getDataElement(nameSpaceName, dataElementName) - var valueString = Cbor.toDiagnostics(valueCbor) - // Workaround for Credman not supporting images yet - if (dataElementName.equals("portrait") || dataElementName.equals("signature_usual_mark")) { - valueString = String.format(Locale.US, "%d bytes", valueCbor.size) - } - val dataElementDisplayName = getDataElementDisplayName(documentInformation.docType, nameSpaceName, dataElementName) - fields.add(IdentityCredentialField( - name = fieldName, - value = valueString, - displayName = dataElementDisplayName, - displayValue = valueString - )) - log("Adding field $fieldName ('$dataElementDisplayName') with value '$valueString'") - } - } - - log("Adding document ${documentInformation.userVisibleName}") - IdentityCredentialEntry( - id = idCount++, - format = "mdoc", - title = documentInformation.userVisibleName, - subtitle = context.getString(R.string.app_name), - icon = BitmapFactory.decodeResource(context.resources, R.drawable.driving_license_bg), - fields = fields.toList(), - disclaimer = null, - warning = null, - ) - } - val registry = IdentityCredentialRegistry(entries) - client.registerCredentials(registry.toRegistrationRequest(context)) - .addOnSuccessListener { log("CredMan registry succeeded") } - .addOnFailureListener { logError("CredMan registry failed $it") } - } - - fun getDocuments(): List { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - return documentStore.listDocuments().mapNotNull { documentName -> - val document = documentStore.lookupDocument(documentName) - document.toDocumentInformation() - } - } - - - fun deleteCredentialByName(documentName: String) { - val document = getDocumentInformation(documentName) - document?.let { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - documentStore.deleteDocument(documentName) - } - registerDocuments() - } - - fun createSelfSignedDocument(documentData: SelfSignedDocumentData) { - val docName = getUniqueDocumentName(documentData) - documentData.provisionInfo.docName = docName - try { - provisionSelfSignedDocument(documentData) - } catch (e: Exception) { - throw IllegalStateException("Error creating self signed document", e) - } - registerDocuments() - } - - private fun getUniqueDocumentName( - documentData: SelfSignedDocumentData, - docName: String = documentData.provisionInfo.docName, - count: Int = 1 - ): String { - val store = ProvisioningUtil.getInstance(context).documentStore - store.listDocuments().forEach { name -> - if (name == docName) { - return getUniqueDocumentName(documentData, "$docName ($count)", count + 1) - } - } - return docName - } - - private fun provisionSelfSignedDocument(documentData: SelfSignedDocumentData) { - val builder = NameSpacedData.Builder() - - for (field in documentData.fields.filter { it.parentId == null }) { - when (field.fieldType) { - is DocumentAttributeType.Date, DocumentAttributeType.DateTime -> { - val date = UnicodeString(field.getValueString()) - date.setTag(1004) - builder.putEntry( - field.namespace!!, - field.name, - FormatUtil.cborEncode(date) - ) - } - - is DocumentAttributeType.Number -> { - builder.putEntryNumber( - field.namespace!!, - field.name, - field.getValueLong() - ) - } - - is DocumentAttributeType.Boolean -> { - builder.putEntryBoolean( - field.namespace!!, - field.name, - field.getValueBoolean() - ) - } - - is DocumentAttributeType.Picture -> { - val bitmap = field.getValueBitmap() - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos) - val bytes = baos.toByteArray() - builder.putEntryByteString(field.namespace!!, field.name, bytes) - } - - is DocumentAttributeType.IntegerOptions -> { - if (field.hasValue()) { - builder.putEntryNumber( - field.namespace!!, - field.name, - field.getValueLong() - ) - } - } - - is DocumentAttributeType.ComplexType -> { - - val dataItem = when (field.isArray) { - true -> { - createArrayDataItem(field, documentData) - } - - false -> { - createMapDataItem(field, documentData) - } - } - builder.putEntry( - field.namespace!!, - field.name, - FormatUtil.cborEncode(dataItem) - ) - } - - else -> { - - builder.putEntryString( - field.namespace!!, - field.name, - field.getValueString() - ) - } - } - } - ProvisioningUtil.getInstance(context) - .provisionSelfSigned(builder.build(), documentData.provisionInfo) - } - - private fun createArrayDataItem(field: Field, documentData: SelfSignedDocumentData): DataItem { - val childFields = documentData.fields.filter { it.parentId == field.id } - val childDataItems = mutableListOf() - - val fieldsPerItem = childFields.distinctBy { it.name }.count() - val itemCount = childFields.count() / fieldsPerItem - - for (i in 0 until itemCount) { - childDataItems.add( - createMapDataItem( - childFields.subList( - i * fieldsPerItem, - (i + 1) * fieldsPerItem - ), documentData - ) - ) - } - - val arrayBuilder = CborBuilder().addArray() - for (childDataItem in childDataItems) { - arrayBuilder.add(childDataItem) - } - return arrayBuilder.end().build()[0] - } - - - private fun createMapDataItem(field: Field, documentData: SelfSignedDocumentData): DataItem { - val childFields = documentData.fields.filter { it.parentId == field.id } - return createMapDataItem(childFields, documentData) - } - - private fun createMapDataItem( - fields: List, - documentData: SelfSignedDocumentData - ): DataItem { - val mapBuilder = CborBuilder().addMap() - for (field in fields) { - when (field.fieldType) { - is DocumentAttributeType.Date, DocumentAttributeType.DateTime -> { - val date = UnicodeString(field.getValueString()) - date.setTag(1004) - mapBuilder.put( - UnicodeString(field.name), - date - ) - } - - is DocumentAttributeType.Boolean -> { - mapBuilder.put( - field.name, - field.getValueBoolean() - ) - } - - is DocumentAttributeType.Picture -> { - val bitmap = field.getValueBitmap() - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos) - val bytes = baos.toByteArray() - mapBuilder.put(field.name, bytes) - } - - is DocumentAttributeType.IntegerOptions, - is DocumentAttributeType.Number -> { - if (field.value != "") { - mapBuilder.put( - field.name, - field.getValueLong() - ) - } - } - - is DocumentAttributeType.ComplexType -> { - val dataItem = when (field.isArray) { - true -> { - createArrayDataItem(field, documentData) - } - - false -> { - createMapDataItem(field, documentData) - } - } - mapBuilder.put(UnicodeString(field.name), dataItem) - } - - else -> { - mapBuilder.put(field.name, field.value as String) - } - } - } - - return mapBuilder.end().build()[0] - } - - fun refreshCredentials(documentName: String) { - val documentInformation = requireNotNull(getDocumentInformation(documentName)) - val document = requireNotNull(getDocumentByName(documentName)) - ProvisioningUtil.getInstance(context).refreshCredentials(document, documentInformation.docType) - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt b/appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt deleted file mode 100644 index a06a881e5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.android.identity.wallet.document - -import android.content.Context -import com.android.identity.wallet.R -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.InputStream -import java.nio.charset.StandardCharsets -import java.security.KeyFactory -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.util.Base64 -object KeysAndCertificates { - - private fun getCertificate(context: Context, resourceId: Int): X509Certificate { - val certInputStream = context.resources.openRawResource(resourceId) - val factory: CertificateFactory = CertificateFactory.getInstance("X509") - return factory.generateCertificate(certInputStream) as X509Certificate - } - - private fun getKeyBytes(keyInputStream: InputStream): ByteArray { - val keyBytes = keyInputStream.readBytes() - val publicKeyPEM = String(keyBytes, StandardCharsets.US_ASCII) - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("\r", "") - .replace("\n", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - - return Base64.getDecoder().decode(publicKeyPEM) - } - - private fun getPrivateKey(context: Context, resourceId: Int): PrivateKey { - val keyBytes: ByteArray = getKeyBytes(context.resources.openRawResource(resourceId)) - val spec = PKCS8EncodedKeySpec(keyBytes) - val kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) - return kf.generatePrivate(spec) - } - - private fun getPublicKey(context: Context, resourceId: Int): PublicKey { - val keyBytes: ByteArray = getKeyBytes(context.resources.openRawResource(resourceId)) - val spec = X509EncodedKeySpec(keyBytes) - val kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) - return kf.generatePublic(spec) - } - - fun getMdlDsKeyPair(context: Context) = - KeyPair( - getPublicKey(context, R.raw.google_mdl_ds_cert_iaca_2_pubkey), - getPrivateKey(context, R.raw.google_mdl_ds_cert_iaca_2_privkey) - ) - - fun getMekbDsKeyPair(context: Context) = - KeyPair( - getPublicKey(context, R.raw.google_mekb_ds_pubkey), - getPrivateKey(context, R.raw.google_mekb_ds_privkey) - ) - - fun getMicovDsKeyPair(context: Context) = - KeyPair( - getPublicKey(context, R.raw.google_micov_ds_pubkey), - getPrivateKey(context, R.raw.google_micov_ds_privkey) - ) - - fun getMdlDsCertificate(context: Context) = getCertificate(context, R.raw.google_mdl_ds_cert_iaca_2) - - fun getMekbDsCertificate(context: Context) = getCertificate(context, R.raw.google_mekb_ds_cert) - - fun getMicovDsCertificate(context: Context) = getCertificate(context, R.raw.google_micov_ds_cert) - - fun getTrustedReaderCertificates(context: Context) = - listOf( - getCertificate(context, R.raw.bdr_iaca_cert), - getCertificate(context, R.raw.bdr_reader_ca_cert), - getCertificate(context, R.raw.credenceid_mdl_reader_cert), - getCertificate(context, R.raw.fast_reader_auth_cer), - getCertificate(context, R.raw.google_reader_ca), - getCertificate(context, R.raw.hid_test_reader_ca_mdl_cert), - getCertificate(context, R.raw.hidtestiacamdl_cert), - getCertificate(context, R.raw.iaca_zetes), - getCertificate(context, R.raw.idemia_brisbane_interop_readerauthca), - getCertificate(context, R.raw.louisiana_department_of_motor_vehicles_cert), - getCertificate(context, R.raw.nist_reader_ca_cer), - getCertificate(context, R.raw.reader_ca_nec_reader_ca_cer), - getCertificate(context, R.raw.samsung_iaca_test_cert), - getCertificate(context, R.raw.scytales_root_ca), - getCertificate(context, R.raw.spruce_iaca_cert), - getCertificate(context, R.raw.ul_cert_ca_01), - getCertificate(context, R.raw.ul_cert_ca_01_cer), - getCertificate(context, R.raw.ul_cert_ca_02), - getCertificate(context, R.raw.ul_cert_ca_03_cer), - getCertificate(context, R.raw.ul_cert_ca_02_cer), - getCertificate(context, R.raw.utms_reader_ca), - getCertificate(context, R.raw.utms_reader_ca_cer), - getCertificate(context, R.raw.zetes_reader_ca), - getCertificate(context, R.raw.zetes_reader_ca_cer), - getCertificate(context, R.raw.owf_identity_credential_reader_cert), - ) - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt deleted file mode 100644 index 6b3f28727..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt +++ /dev/null @@ -1,261 +0,0 @@ -package com.android.identity.wallet.documentdata - -import android.graphics.BitmapFactory -import co.nstant.`in`.cbor.CborDecoder -import co.nstant.`in`.cbor.CborException -import co.nstant.`in`.cbor.model.AbstractFloat -import co.nstant.`in`.cbor.model.Array -import co.nstant.`in`.cbor.model.ByteString -import co.nstant.`in`.cbor.model.DataItem -import co.nstant.`in`.cbor.model.DoublePrecisionFloat -import co.nstant.`in`.cbor.model.MajorType -import co.nstant.`in`.cbor.model.Map -import co.nstant.`in`.cbor.model.NegativeInteger -import co.nstant.`in`.cbor.model.SimpleValue -import co.nstant.`in`.cbor.model.SimpleValueType -import co.nstant.`in`.cbor.model.UnicodeString -import co.nstant.`in`.cbor.model.UnsignedInteger -import com.android.identity.document.NameSpacedData -import com.android.identity.wallet.util.DocumentData.EU_PID_DOCTYPE -import com.android.identity.wallet.util.DocumentData.EU_PID_NAMESPACE -import com.android.identity.wallet.util.DocumentData.MDL_DOCTYPE -import com.android.identity.wallet.util.DocumentData.MDL_NAMESPACE -import com.android.identity.wallet.util.DocumentData.MICOV_ATT_NAMESPACE -import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE -import java.io.ByteArrayInputStream -import java.math.BigInteger -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.util.Locale - -class DocumentDataReader(private val docType: String) { - - fun read(nameSpacedData: NameSpacedData): DocumentElements { - val builder = StringBuilder() - var portraitBytes: ByteArray? = null - var signatureBytes: ByteArray? = null - var fingerprintBytes: ByteArray? = null - - nameSpacedData.nameSpaceNames.forEach { namespace -> - val dataElementNames = nameSpacedData.getDataElementNames(namespace) - builder.append("
") - builder.append("
Namespace: $namespace
") - builder.append("

") - dataElementNames.forEach { entryName -> - val byteArray: ByteArray = nameSpacedData.getDataElement(namespace, entryName) - byteArray.let { value -> - val valueStr: String - if (isPortraitElement(docType, namespace, entryName)) { - valueStr = String.format("(%d bytes, shown above)", value.size) - portraitBytes = nameSpacedData.getDataElementByteString(namespace, entryName) - } else if (docType == MICOV_DOCTYPE && namespace == MICOV_ATT_NAMESPACE && entryName == "fac") { - valueStr = String.format("(%d bytes, shown above)", value.size) - portraitBytes = nameSpacedData.getDataElement(namespace, entryName) - } else if (docType == MDL_DOCTYPE && namespace == MDL_NAMESPACE && entryName == "extra") { - valueStr = String.format("%d bytes extra data", value.size) - } else if (docType == MDL_DOCTYPE && namespace == MDL_NAMESPACE && entryName == "signature_usual_mark") { - valueStr = String.format("(%d bytes, shown below)", value.size) - signatureBytes = nameSpacedData.getDataElementByteString(namespace, entryName) - } else if (docType == EU_PID_DOCTYPE && namespace == EU_PID_NAMESPACE && entryName == "biometric_template_finger") { - valueStr = String.format("(%d bytes, not shown)", value.size) - fingerprintBytes = nameSpacedData.getDataElementByteString(namespace, entryName) - } else { - valueStr = cborPrettyPrint(value) - } - builder.append("$entryName -> $valueStr
") - } - builder.append("


") - } - } - val portrait = portraitBytes?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - val signature = signatureBytes?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - val fingerprint = fingerprintBytes?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - - return DocumentElements( - text = builder.toString(), - portrait = portrait, - signature = signature, - fingerprint = fingerprint - ) - } - - private fun isPortraitElement( - docType: String, - namespace: String?, - entryName: String? - ): Boolean { - val hasPortrait = docType == MDL_DOCTYPE || docType == EU_PID_DOCTYPE - val namespaceContainsPortrait = namespace == MDL_NAMESPACE || namespace == EU_PID_NAMESPACE - return hasPortrait && namespaceContainsPortrait && entryName == "portrait" - } - - private fun cborPrettyPrint(encodedBytes: ByteArray): String { - val newLine = "
" - val sb = java.lang.StringBuilder() - val bais = ByteArrayInputStream(encodedBytes) - val dataItems = try { - CborDecoder(bais).decode() - } catch (e: CborException) { - throw java.lang.IllegalStateException(e) - } - for ((count, dataItem) in dataItems.withIndex()) { - if (count > 0) { - sb.append(",$newLine") - } - cborPrettyPrintDataItem(sb, 0, dataItem) - } - return sb.toString() - } - - private fun cborPrettyPrintDataItem( - sb: java.lang.StringBuilder, indent: Int, - dataItem: DataItem - ) { - val space = " " - val newLine = "
" - val indentBuilder = java.lang.StringBuilder() - for (n in 0 until indent) { - indentBuilder.append(space) - } - val indentString = indentBuilder.toString() - if (dataItem.hasTag()) { - sb.append(String.format("tag %d ", dataItem.tag.value)) - } - when (dataItem.majorType) { - MajorType.INVALID -> // TODO: throw - sb.append("**invalid**") - - MajorType.UNSIGNED_INTEGER -> { - // Major type 0: an unsigned integer. - val value: BigInteger = (dataItem as UnsignedInteger).value - sb.append(value) - } - - MajorType.NEGATIVE_INTEGER -> { - // Major type 1: a negative integer. - val value: BigInteger = (dataItem as NegativeInteger).value - sb.append(value) - } - - MajorType.BYTE_STRING -> { - // Major type 2: a byte string. - val value = (dataItem as ByteString).bytes - sb.append("[") - for ((count, b) in value.withIndex()) { - if (count > 0) { - sb.append(", ") - } - sb.append(String.format("0x%02x", b)) - } - sb.append("]") - } - - MajorType.UNICODE_STRING -> { - // Major type 3: string of Unicode characters that is encoded as UTF-8 [RFC3629]. - val value = (dataItem as UnicodeString).string - // TODO: escape ' in |value| - sb.append("'$value'") - } - - MajorType.ARRAY -> { - - // Major type 4: an array of data items. - val items = (dataItem as Array).dataItems - if (items.size == 0) { - sb.append("[]") - } else if (cborAreAllDataItemsNonCompound(items)) { - // The case where everything fits on one line. - sb.append("[") - for ((count, item) in items.withIndex()) { - cborPrettyPrintDataItem(sb, indent, item) - if (count + 1 < items.size) { - sb.append(", ") - } - } - sb.append("]") - } else { - sb.append("[$newLine$indentString") - for ((count, item) in items.withIndex()) { - sb.append("$space$space") - cborPrettyPrintDataItem(sb, indent + 2, item) - if (count + 1 < items.size) { - sb.append(",") - } - sb.append("$newLine $indentString") - } - sb.append("]") - } - } - - MajorType.MAP -> { - // Major type 5: a map of pairs of data items. - val keys = (dataItem as Map).keys - if (keys.isEmpty()) { - sb.append("{}") - } else { - sb.append("{$newLine$indentString") - for ((count, key) in keys.withIndex()) { - sb.append("$space$space") - val value = dataItem[key] - cborPrettyPrintDataItem(sb, indent + 2, key) - sb.append(" : ") - cborPrettyPrintDataItem(sb, indent + 2, value) - if (count + 1 < keys.size) { - sb.append(",") - } - sb.append("$newLine $indentString") - } - sb.append("}") - } - } - - MajorType.TAG -> throw java.lang.IllegalStateException("Semantic tag data item not expected") - MajorType.SPECIAL -> // Major type 7: floating point numbers and simple data types that need no - // content, as well as the "break" stop code. - if (dataItem is SimpleValue) { - when (dataItem.simpleValueType) { - SimpleValueType.FALSE -> sb.append("false") - SimpleValueType.TRUE -> sb.append("true") - SimpleValueType.NULL -> sb.append("null") - SimpleValueType.UNDEFINED -> sb.append("undefined") - SimpleValueType.RESERVED -> sb.append("reserved") - SimpleValueType.UNALLOCATED -> sb.append("unallocated") - } - } else if (dataItem is DoublePrecisionFloat) { - val df = DecimalFormat( - "0", - DecimalFormatSymbols.getInstance(Locale.ENGLISH) - ) - df.maximumFractionDigits = 340 - sb.append(df.format(dataItem.value)) - } else if (dataItem is AbstractFloat) { - val df = DecimalFormat( - "0", - DecimalFormatSymbols.getInstance(Locale.ENGLISH) - ) - df.maximumFractionDigits = 340 - sb.append(df.format(dataItem.value)) - } else { - sb.append("break") - } - } - } - - // Returns true iff all elements in |items| are not compound (e.g. an array or a map). - private fun cborAreAllDataItemsNonCompound(items: List): Boolean { - for (item in items) { - when (item.majorType) { - MajorType.ARRAY, MajorType.MAP -> return false - else -> { - } - } - } - return true - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt deleted file mode 100644 index 322eff249..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.identity.wallet.documentdata - -import android.graphics.Bitmap - -data class DocumentElements( - val text: String = "", - val portrait: Bitmap? = null, - val signature: Bitmap? = null, - val fingerprint: Bitmap? = null, - val requestUserAuthorization: Boolean = false, - val passphrase: String = "" -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt deleted file mode 100644 index d0f477a85..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt +++ /dev/null @@ -1,225 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.StringOption - -object DrivingLicense { - const val MDL_NAMESPACE = "org.iso.18013.5.1" - const val AAMVA_NAMESPACE = "org.iso.18013.5.1.aamva" - fun getMdocComplexTypes() = MdocComplexTypes.Builder("org.iso.18013.5.1.mDL") - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "vehicle_category_code", - "Vehicle Category Code", - DocumentAttributeType.StringOptions( - listOf( - StringOption(null, "(not set)"), - StringOption("A", "Motorcycles (A)"), - StringOption("AEU", "Motorcycles (AEU)"), - StringOption("B", "Light vehicles (B"), - StringOption("C", "Goods vehicles (C)"), - StringOption("D", "Passenger vehicles (D)"), - StringOption("BE", "Light vehicles with trailers (BE)"), - StringOption("CE", "Goods vehicles with trailers (CE)"), - StringOption("DE", "Passenger vehicles with trailers (DE)"), - StringOption("AM", "Mopeds (AM)"), - StringOption("A1", "Light motorcycles (A1)"), - StringOption("A1EU", "Light motorcycles (A1EU)"), - StringOption("A2", "Medium motorcycles (A2)"), - StringOption("B1", "Light vehicles (B1)"), - StringOption("B1EU", "Light vehicles (B1EU)"), - StringOption("C1", "Medium sized goods vehicles (C1)"), - StringOption("D1", "Medium sized passenger vehicles (e.g. minibuses) (D1)"), - ) - ) - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "issue_date", - "Date of Issue", - DocumentAttributeType.Date - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "expiry_date", - "Date of Expiry", - DocumentAttributeType.Date - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "codes", - "Codes of Driving Privileges", - DocumentAttributeType.ComplexType, - ) - // details of DrivingPrivilege.codes - .addDefinition( - MDL_NAMESPACE, - hashSetOf("codes"), - true, - "code", - "Code", - DocumentAttributeType.StringOptions( - listOf( - StringOption(null, "(not set)"), - StringOption( - "01", - "Licence holder requires eye sight correction and/or protection" - ), - StringOption( - "03", - "Licence holder requires prosthetic device for the limbs" - ), - StringOption( - "78", - "Licence holder restricted to vehicles with automatic transmission" - ), - StringOption( - "S01", - "The vehicle's maximum authorized mass (kg) shall be" - ), - StringOption( - "S02", - "The vehicle's authorized passenger seats, excluding the driver's seat, shall be" - ), - StringOption( - "S03", - "The vehicle's cylinder capacity (cm3) shall be" - ), - StringOption( - "S04", - "The vehicle's power (kW) shall be" - ), - StringOption( - "S05", - "Licence holder restricted to vehicles adapted for physically disabled" - ) - ) - ) - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("codes"), - true, - "sign", - "Sign", - DocumentAttributeType.StringOptions( - listOf( - StringOption(null, "(not set)"), - StringOption("=", "Equals (=)"), - StringOption(">", "Greater than (>)"), - StringOption("<", "Less than (<)"), - StringOption(">=", "Greater than or equal to (≥)"), - StringOption("<=", "Less than or equal to (≤)") - ) - ) - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("codes"), - true, - "value", - "Value", - DocumentAttributeType.String - ). - // details of domestic_driving_privileges - addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_driving_privileges"), - true, - "domestic_vehicle_class", - "Domestic Vehicle Class", - DocumentAttributeType.ComplexType, - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_driving_privileges"), - true, - "domestic_vehicle_restrictions", - "Domestic Vehicle Restrictions", - DocumentAttributeType.ComplexType - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_driving_privileges"), - true, - "domestic_vehicle_endorsements", - "Domestic Vehicle Endorsements", - DocumentAttributeType.ComplexType - ) - // details of DomesticDrivingPrivilege.domestic_vehicle_class - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "domestic_vehicle_class_code", - "Domestic Vehicle Class Code", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "domestic_vehicle_class_description", - "Domestic Vehicle Class Description", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "issue_date", - "Date of Issue", - DocumentAttributeType.Date - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "expiry_date", - "Date of Expiry", - DocumentAttributeType.Date - ) - // details of DomesticDrivingPrivilege.domestic_vehicle_restrictions - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_restrictions"), - true, - "domestic_vehicle_restriction_code", - "Restriction Code", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_restrictions"), - true, - "domestic_vehicle_restriction_description", - "Vehicle Category Description", - DocumentAttributeType.String - ) - // details of DomesticDrivingPrivilege.domestic_vehicle_endorsements - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_endorsements"), - true, - "domestic_vehicle_endorsement_code", - "Endorsement Code", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_endorsements"), - true, - "domestic_vehicle_endorsement_description", - "Vehicle Endorsement Description", - DocumentAttributeType.String - ) - .build() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt deleted file mode 100644 index d5026e6fd..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType - -data class MdocComplexTypeDefinition( - val parentIdentifiers: HashSet, - val partOfArray: Boolean, - val identifier: String, - val displayName: String, - val type: DocumentAttributeType -) diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt deleted file mode 100644 index 3e48f8830..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.documentdata - -object MdocComplexTypeRepository { - private val allComplexTypes: MutableMap = mutableMapOf() - - init { - addComplexTypes(DrivingLicense.getMdocComplexTypes()) - addComplexTypes(VehicleRegistration.getMdocComplexTypes()) - addComplexTypes(VaccinationDocument.getMdocComplexTypes()) - } - - fun addComplexTypes(complexTypes: MdocComplexTypes) { - allComplexTypes[complexTypes.docType] = complexTypes - } - - fun getComplexTypes(docType: String): MdocComplexTypes? { - return allComplexTypes[docType] - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt deleted file mode 100644 index a52e608e9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType - -class MdocComplexTypes private constructor( - val docType: String, - val namespaces: List -) { - - data class Builder( - val docType: String, - val namespaces: MutableMap = mutableMapOf() - ) { - fun addDefinition( - namespace: String, - parentIdentifiers: HashSet, - partOfArray: Boolean, - identifier: String, - displayName: String, - type: DocumentAttributeType - ) = apply { - if (!namespaces.containsKey(namespace)) { - namespaces[namespace] = MdocNamespaceComplexTypes.Builder(namespace) - } - namespaces[namespace]!!.addDefinition(parentIdentifiers, partOfArray, identifier, displayName, type) - } - - fun build() = MdocComplexTypes(docType, namespaces.values.map { it.build() }) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt deleted file mode 100644 index ae68f8021..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType - -class MdocNamespaceComplexTypes( - val namespace: String, - val dataElements: List -) { - data class Builder( - val namespace: String, - val dataElements: MutableList = mutableListOf() - ) { - fun addDefinition( - parentIdentifiers: HashSet, - partOfArray: Boolean, - identifier: String, - displayName: String, - type: DocumentAttributeType - ) = apply { - dataElements.add(MdocComplexTypeDefinition(parentIdentifiers, partOfArray, identifier, displayName, type)) - } - - fun build() = MdocNamespaceComplexTypes(namespace, dataElements) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt deleted file mode 100644 index 148ad9fa0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.android.identity.wallet.documentdata - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.text.HtmlCompat -import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.databinding.FragmentShowDocumentDataBinding -import com.android.identity.wallet.transfer.TransferManager - -class ShowDocumentDataFragment : Fragment() { - - private val arguments by navArgs() - private var _binding: FragmentShowDocumentDataBinding? = null - private val binding get() = _binding!! - private lateinit var transferManager: TransferManager - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - transferManager = TransferManager.getInstance(requireContext()) - } - - override fun onDestroy() { - super.onDestroy() - transferManager.destroy() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentShowDocumentDataBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - loadDocumentElements() - } - - private fun loadDocumentElements() { - transferManager.readDocumentEntries(arguments.documentName).let { result -> - binding.tvResults.text = HtmlCompat.fromHtml(result.text, FROM_HTML_MODE_LEGACY) - result.portrait?.let { portrait -> - binding.ivPortrait.setImageBitmap(portrait) - binding.ivPortrait.visibility = View.VISIBLE - } - result.signature?.let { signature -> - binding.ivSignature.setImageBitmap(signature) - binding.ivSignature.visibility = View.VISIBLE - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt deleted file mode 100644 index bd4798ee3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.knowntypes.Options - -object VaccinationDocument { - const val MICOV_ATT_NAMESPACE = "org.micov.attestation.1" - const val MICOV_VTR_NAMESPACE = "org.micov.vtr.1" - fun getMdocComplexTypes() = MdocComplexTypes.Builder("org.micov.1") - - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("RA01_test"), - false, - "Result", - "Test Result", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("RA01_test"), - false, - "TypeOfTest", - "Type of Test", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("RA01_test"), - false, - "TimeOfTest", - "Time of Test", - DocumentAttributeType.DateTime - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("safeEntry_Leisure"), - false, - "SeCondFulfilled", - "Second Fulfilled", - DocumentAttributeType.Boolean - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("safeEntry_Leisure"), - false, - "SeCondType", - "Second Type", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("safeEntry_Leisure"), - false, - "SeCondExpiry", - "Second Expiry", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "tg", - "Disease or Agent Targeted", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "vp", - "Vaccine or Prophylaxis", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "mp", - "Vaccine Medicinal Product", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "br", - "Vaccine Brand", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ma", - "Manufacturer", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "bn", - "Batch/Lot Number of the Vaccine", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "dn", - "Dose Number", - DocumentAttributeType.Number - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "sd", - "Total Series of Doses", - DocumentAttributeType.Number - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "dt", - "Date of Vaccination", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "co", - "Country of Vaccination", - DocumentAttributeType.StringOptions(Options.COUNTRY_ISO_3166_1_ALPHA_2) - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ao", - "Administering Organization", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ap", - "Administering Professional", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "nx", - "Due Date of Next Dose", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "is", - "Certificate Issuer", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ci", - "Unique Certificate Identifier (UVCI)", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "pd", - "Protection Duration", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "vf", - "Valid From", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "vu", - "Valid Until", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pty", - "Type of Person Identifier", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pnr", - "Unique number for the PTY/PIC/(PIA) combination", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pic", - "Issuing Country of the PTY", - DocumentAttributeType.StringOptions(Options.COUNTRY_ISO_3166_1_ALPHA_2) - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pia", - "Issuing Authority of the PTY", - DocumentAttributeType.String, - ) - .build() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt deleted file mode 100644 index 3b3c64556..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.knowntypes.Options - -object VehicleRegistration { - const val MVR_NAMESPACE = "nl.rdw.mekb.1" - fun getMdocComplexTypes() = MdocComplexTypes.Builder("nl.rdw.mekb.1") - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "issuingCountry", - "Country Code", - DocumentAttributeType.StringOptions(Options.COUNTRY_ISO_3166_1_ALPHA_2) - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "competentAuthority", - "Competent Authority", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "registrationNumber", - "UN/EU Element A", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "validFrom", - "Custom EKB Element, Valid From", - DocumentAttributeType.Date - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "validUntil", - "Custom EKB Element, Valid Until", - DocumentAttributeType.Date - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_holder"), - false, - "holderInfo", - "Personal Data", - DocumentAttributeType.ComplexType - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_holder"), - false, - "ownershipStatus", - "Ownership Status", - DocumentAttributeType.Number - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("holderInfo"), - false, - "name", - "Name of the Vehicle Owner", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("holderInfo"), - false, - "address", - "Address of the Vehicle Owner", - DocumentAttributeType.ComplexType - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "streetName", - "Street Name", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "houseNumber", - "House Number", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "houseNumberSuffix", - "House Number Suffix", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "postalCode", - "Postal Code", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "placeOfResidence", - "Place of Residence", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("basic_vehicle_info"), - false, - "vehicle", - "Vehicle", - DocumentAttributeType.ComplexType - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("vehicle"), - false, - "make", - "Make of the Vehicle", - DocumentAttributeType.String, - ) - - .build() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt deleted file mode 100644 index 9581661d0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt +++ /dev/null @@ -1,458 +0,0 @@ -package com.android.identity.wallet.documentinfo - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -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.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Key -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.RemoveRedEye -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.crypto.EcCurve -import com.android.identity.securearea.KeyPurpose -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.LoadingIndicator -import com.android.identity.wallet.composables.ShowToast -import com.android.identity.wallet.composables.gradientFor -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun DocumentInfoScreen( - viewModel: DocumentInfoViewModel, - onNavigateUp: () -> Unit, - onNavigateToDocumentDetails: () -> Unit -) { - val state by viewModel.screenState.collectAsState() - if (state.isDeleted) { - ShowToast(message = stringResource(id = R.string.delete_document_deleted_message)) - onNavigateUp() - } - - DocumentInfoScreenContent( - screenState = state, - onRefreshCredentials = viewModel::refreshCredentials, - onShowDocumentElements = { onNavigateToDocumentDetails() }, - onDeleteDocument = { viewModel.promptDocumentDelete() }, - onConfirmDocumentDelete = viewModel::confirmDocumentDelete, - onCancelDocumentDelete = viewModel::cancelDocumentDelete - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun DocumentInfoScreenContent( - modifier: Modifier = Modifier, - screenState: DocumentInfoScreenState, - onRefreshCredentials: () -> Unit, - onShowDocumentElements: () -> Unit, - onDeleteDocument: () -> Unit, - onConfirmDocumentDelete: () -> Unit, - onCancelDocumentDelete: () -> Unit, -) { - Scaffold( - modifier = modifier - ) { paddingValues -> - Column( - modifier = Modifier.padding(paddingValues), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = CenterHorizontally - ) { - if (screenState.isLoading) { - LoadingIndicator( - modifier = Modifier.fillMaxSize() - ) - } else { - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxWidth()) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .border( - 2.dp, - gradientFor(screenState.documentColor), - RoundedCornerShape(12.dp) - ) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - LabeledValue( - label = stringResource(id = R.string.label_document_name), - value = screenState.documentName - ) - LabeledValue( - label = stringResource(id = R.string.label_document_type), - value = screenState.documentType - ) - LabeledValue( - label = stringResource(id = R.string.label_date_provisioned), - value = screenState.provisioningDate - ) - LabeledValue( - label = stringResource(id = R.string.label_last_time_used), - value = screenState.lastTimeUsedDate.ifBlank { "N/A" } - ) - LabeledValue( - label = stringResource(id = R.string.label_issuer), - value = if (screenState.isSelfSigned) "Self-Signed on Device" else "N/A" - ) - } - } - val pagerState = rememberPagerState(pageCount = { screenState.authKeys.size }) - HorizontalPager( - modifier = Modifier - .fillMaxWidth(), - state = pagerState, - ) { page -> - val key = screenState.authKeys[page] - AuthenticationKeyInfo( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer), - authKeyInfo = key - ) - } - PagerIndicators( - modifier = Modifier - .height(24.dp) - .fillMaxWidth() - .align(CenterHorizontally), - pagerState = pagerState, - itemsCount = screenState.authKeys.size, - ) - Divider(modifier = Modifier.padding(top = 12.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .weight(1f) - .clickable { onRefreshCredentials() }, - horizontalAlignment = CenterHorizontally - ) { - Column( - modifier = Modifier.padding(12.dp), - horizontalAlignment = CenterHorizontally - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = stringResource(id = R.string.bt_refresh_auth_keys), - tint = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(id = R.string.bt_refresh_auth_keys), - style = MaterialTheme.typography.bodySmall - ) - } - } - Column( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .weight(1f) - .clickable { onShowDocumentElements() }, - horizontalAlignment = CenterHorizontally - ) { - Column( - modifier = Modifier.padding(12.dp), - horizontalAlignment = CenterHorizontally - ) { - Icon( - imageVector = Icons.Default.RemoveRedEye, - contentDescription = stringResource(id = R.string.bt_show_data), - tint = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(id = R.string.bt_show_data), - style = MaterialTheme.typography.bodySmall - ) - } - } - } - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 24.dp) - .align(Alignment.BottomCenter), - onClick = onDeleteDocument, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), - colors = ButtonDefaults.outlinedButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(id = R.string.bt_delete), - ) - Text(text = stringResource(id = R.string.bt_delete)) - } - } - if (screenState.isDeletingPromptShown) { - DeleteDocumentPrompt( - onConfirm = onConfirmDocumentDelete, - onCancel = onCancelDocumentDelete - ) - } - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun PagerIndicators( - modifier: Modifier = Modifier, - pagerState: PagerState, - itemsCount: Int, -) { - Row( - modifier, - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - repeat(itemsCount) { iteration -> - val color = if (pagerState.currentPage == iteration) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .2f) - } - Box( - modifier = Modifier - .padding(2.dp) - .clip(CircleShape) - .background(color) - .size(12.dp) - ) - } - } -} - -@Composable -private fun AuthenticationKeyInfo( - modifier: Modifier = Modifier, - authKeyInfo: DocumentInfoScreenState.KeyInformation -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier - .size(48.dp) - .padding(horizontal = 8.dp), - imageVector = Icons.Default.Key, - contentDescription = "${authKeyInfo.counter}", - tint = MaterialTheme.colorScheme.primary.copy(alpha = .5f) - ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - LabeledValue( - label = stringResource(id = R.string.txt_keystore_implementation), - value = authKeyInfo.secureAreaDisplayName - ) - LabeledValue( - label = stringResource(id = R.string.document_info_counter), - value = "${authKeyInfo.counter}" - ) - LabeledValue( - label = stringResource(id = R.string.document_info_domain), - value = authKeyInfo.domain - ) - LabeledValue( - label = stringResource(id = R.string.document_info_valid_from), - value = authKeyInfo.validFrom - ) - LabeledValue( - label = stringResource(id = R.string.document_info_valid_until), - value = authKeyInfo.validUntil - ) - LabeledValue( - label = stringResource(id = R.string.document_info_issuer_data), - value = stringResource( - id = R.string.document_info_issuer_data_bytes, - authKeyInfo.issuerDataBytesCount - ) - ) - LabeledValue( - label = stringResource(id = R.string.document_info_usage_count), - value = "${authKeyInfo.usagesCount}" - ) - LabeledValue( - label = stringResource(id = R.string.document_info_key_purposes), - value = authKeyInfo.keyPurposes.toString() - ) - LabeledValue( - label = stringResource(id = R.string.document_info_ec_curve), - value = authKeyInfo.ecCurve.toString() - ) - } - } -} - -@Composable -private fun DeleteDocumentPrompt( - onConfirm: () -> Unit, - onCancel: () -> Unit -) { - AlertDialog( - onDismissRequest = onCancel, - title = { - Text(text = stringResource(id = R.string.delete_document_prompt_title)) - }, - text = { - Text(text = stringResource(id = R.string.delete_document_prompt_message)) - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(text = stringResource(id = R.string.bt_delete)) - } - }, - dismissButton = { - TextButton(onClick = onCancel) { - Text(text = stringResource(id = R.string.bt_cancel)) - } - } - ) -} - -@Composable -private fun LabeledValue( - modifier: Modifier = Modifier, - label: String, - value: String -) { - val textValue = buildAnnotatedString { - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(label) - append(": ") - } - append(value) - } - Text( - modifier = modifier, - text = textValue, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) -} - -@Composable -@Preview -private fun PreviewDocumentInfoScreenLoading() { - HolderAppTheme { - DocumentInfoScreenContent( - screenState = DocumentInfoScreenState( - isLoading = true - ), - onRefreshCredentials = {}, - onShowDocumentElements = {}, - onDeleteDocument = {}, - onConfirmDocumentDelete = {} - ) {} - } -} - -@Composable -@Preview -private fun PreviewDocumentInfoScreen() { - HolderAppTheme { - DocumentInfoScreenContent( - screenState = DocumentInfoScreenState( - documentName = "Erica's Driving Licence", - documentType = "org.iso.18013.5.1.mDL", - provisioningDate = "16-07-2023", - isSelfSigned = true, - authKeys = listOf( - DocumentInfoScreenState.KeyInformation( - counter = 1, - validFrom = "16-07-2023", - validUntil = "23-07-2023", - domain = "Domain", - usagesCount = 1, - issuerDataBytesCount = "Issuer 1".toByteArray().count(), - keyPurposes = KeyPurpose.AGREE_KEY, - ecCurve = EcCurve.P256, - isHardwareBacked = false, - secureAreaDisplayName = "Secure Area Name" - ), - DocumentInfoScreenState.KeyInformation( - counter = 2, - validFrom = "16-07-2023", - validUntil = "23-07-2023", - domain = "Domain", - usagesCount = 0, - issuerDataBytesCount = "Issuer 2".toByteArray().count(), - keyPurposes = KeyPurpose.SIGN, - ecCurve = EcCurve.ED25519, - isHardwareBacked = true, - secureAreaDisplayName = "Secure Area Name" - - ) - ) - ), - onRefreshCredentials = {}, - onShowDocumentElements = {}, - onDeleteDocument = {}, - onConfirmDocumentDelete = {} - ) {} - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt deleted file mode 100644 index cde989dff..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.identity.wallet.documentinfo - -import androidx.compose.runtime.Immutable -import com.android.identity.crypto.EcCurve -import com.android.identity.securearea.KeyPurpose -import com.android.identity.wallet.document.DocumentColor - -@Immutable -data class DocumentInfoScreenState( - val isLoading: Boolean = false, - val documentName: String = "", - val documentType: String = "", - val documentColor: DocumentColor = DocumentColor.Green, - val provisioningDate: String = "", - val lastTimeUsedDate: String = "", - val isSelfSigned: Boolean = false, - val authKeys: List = emptyList(), - val isDeletingPromptShown: Boolean = false, - val isDeleted: Boolean = false -) { - - @Immutable - data class KeyInformation( - val counter: Int, - val validFrom: String, - val validUntil: String, - val domain: String, - val issuerDataBytesCount: Int, - val usagesCount: Int, - val keyPurposes: KeyPurpose, - val ecCurve: EcCurve, - val isHardwareBacked: Boolean, - val secureAreaDisplayName: String - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt deleted file mode 100644 index b5a27a4de..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.android.identity.wallet.documentinfo - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import com.android.identity.wallet.composables.toCardArt -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.fragment.DocumentDetailFragmentArgs -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DocumentInfoViewModel( - private val documentManager: DocumentManager, - savedStateHandle: SavedStateHandle -) : ViewModel() { - - private val args = DocumentDetailFragmentArgs.fromSavedStateHandle(savedStateHandle) - private val _state = MutableStateFlow(DocumentInfoScreenState()) - val screenState: StateFlow = _state.asStateFlow() - - fun loadDocument(documentName: String) { - _state.update { it.copy(isLoading = true) } - viewModelScope.launch { - val documentInfo = withContext(Dispatchers.IO) { - documentManager.getDocumentInformation(documentName) - } - onDocumentInfoLoaded(documentInfo) - } - } - - fun promptDocumentDelete() { - _state.update { it.copy(isDeletingPromptShown = true) } - } - - fun confirmDocumentDelete() { - documentManager.deleteCredentialByName(args.documentName) - _state.update { it.copy(isDeleted = true, isDeletingPromptShown = false) } - } - - fun cancelDocumentDelete() { - _state.update { it.copy(isDeletingPromptShown = false) } - } - - fun refreshCredentials() { - _state.update { it.copy(isLoading = true) } - viewModelScope.launch { - withContext(Dispatchers.IO) { - documentManager.refreshCredentials(args.documentName) - } - loadDocument(args.documentName) - } - } - - private fun onDocumentInfoLoaded(documentInformation: DocumentInformation?) { - documentInformation?.let { - _state.update { - it.copy( - isLoading = false, - documentName = documentInformation.userVisibleName, - documentType = documentInformation.docType, - documentColor = documentInformation.documentColor.toCardArt(), - provisioningDate = documentInformation.dateProvisioned, - isSelfSigned = documentInformation.selfSigned, - lastTimeUsedDate = documentInformation.lastTimeUsed, - authKeys = documentInformation.authKeys.asScreenStateKeys() - ) - } - } - } - - private fun List.asScreenStateKeys(): List { - return map { keyData -> - DocumentInfoScreenState.KeyInformation( - counter = keyData.counter, - validFrom = keyData.validFrom, - validUntil = keyData.validUntil, - domain = keyData.domain, - issuerDataBytesCount = keyData.issuerDataBytesCount, - usagesCount = keyData.usagesCount, - keyPurposes = keyData.keyPurposes, - ecCurve = keyData.ecCurve, - isHardwareBacked = keyData.isHardwareBacked, - secureAreaDisplayName = keyData.secureAreaDisplayName - ) - } - } - - companion object { - fun Factory(documentManager: DocumentManager): ViewModelProvider.Factory = - viewModelFactory { - initializer { - DocumentInfoViewModel(documentManager, createSavedStateHandle()) - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt deleted file mode 100644 index 36e001cf0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.documentinfo.DocumentInfoScreen -import com.android.identity.wallet.documentinfo.DocumentInfoViewModel -import com.android.identity.wallet.theme.HolderAppTheme - -class DocumentDetailFragment : Fragment() { - - private val args: DocumentDetailFragmentArgs by navArgs() - private val viewModel by viewModels { - val documentManager = DocumentManager.getInstance(requireContext()) - DocumentInfoViewModel.Factory(documentManager) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - DocumentInfoScreen( - viewModel = viewModel, - onNavigateUp = { findNavController().navigateUp() }, - onNavigateToDocumentDetails = { onShowData(args.documentName) } - ) - } - } - viewModel.loadDocument(args.documentName) - } - } - - private fun onShowData(documentName: String) { - val direction = DocumentDetailFragmentDirections - .navigateToDocumentData(documentName) - findNavController().navigate(direction) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt deleted file mode 100644 index 22ce77a2a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.wallet.databinding.FragmentReverseEngagementBinding -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel -import com.budiyev.android.codescanner.CodeScanner -import com.budiyev.android.codescanner.DecodeCallback - -class ReverseEngagementFragment : Fragment() { - - private var _binding: FragmentReverseEngagementBinding? = null - private var codeScanner: CodeScanner? = null - - private val binding get() = _binding!! - private val vm: ShareDocumentViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentReverseEngagementBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.btCancel.setOnClickListener { - findNavController().navigate( - ReverseEngagementFragmentDirections.actionReverseEngagementFragmentToSelectDocumentFragment() - ) - } - - codeScanner = CodeScanner(requireContext(), binding.csScanner) - codeScanner?.decodeCallback = DecodeCallback { result -> - requireActivity().runOnUiThread { - val qrText = result.text - log("qrText: $qrText") - val uri = Uri.parse(qrText) - if (uri.scheme.equals("mdoc")) { - vm.startPresentationReverseEngagement(qrText, emptyList()) - findNavController().navigate( - ReverseEngagementFragmentDirections.actionReverseEngagementFragmentToTransferDocumentFragment() - ) - } else { - logWarning("Ignoring QR code with scheme " + uri.scheme) - } - } - } - binding.csScanner.setOnClickListener { codeScanner?.startPreview() } - } - - override fun onResume() { - super.onResume() - enableReader() - } - - override fun onPause() { - super.onPause() - disableReader() - } - - private fun enableReader() { - if (isAllPermissionsGranted()) { - codeScanner?.startPreview() - } else { - shouldRequestPermission() - } - } - - private fun disableReader() { - codeScanner?.releaseResources() - } - - private val appPermissions: List - get() = mutableListOf(Manifest.permission.CAMERA) - - private fun shouldRequestPermission() { - val permissionsNeeded = appPermissions.filter { permission -> - ContextCompat.checkSelfPermission( - requireContext(), - permission - ) != PackageManager.PERMISSION_GRANTED - } - - if (permissionsNeeded.isNotEmpty()) { - permissionsLauncher.launch( - permissionsNeeded.toTypedArray() - ) - } - } - - private fun isAllPermissionsGranted(): Boolean { - // If any permission is not granted return false - return appPermissions.none { permission -> - ContextCompat.checkSelfPermission( - requireContext(), - permission - ) != PackageManager.PERMISSION_GRANTED - } - } - - private val permissionsLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - log("permissionsLauncher ${it.key} = ${it.value}") - // Open settings if user denied any required permission - if (!it.value && !shouldShowRequestPermissionRationale(it.key)) { - openSettings() - return@registerForActivityResult - } - } - } - - private fun openSettings() { - val intent = Intent() - intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - intent.data = Uri.fromParts("package", requireContext().packageName, null) - startActivity(intent) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt deleted file mode 100644 index 0a92ebd7e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt +++ /dev/null @@ -1,558 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import android.graphics.Typeface -import android.icu.text.SimpleDateFormat -import android.net.Uri -import android.os.Bundle -import android.os.Environment -import android.provider.MediaStore -import android.text.Editable -import android.text.InputType -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.View -import android.view.View.NOT_FOCUSABLE -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.widget.ArrayAdapter -import android.widget.CheckBox -import android.widget.EditText -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.Spinner -import android.widget.TextView -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission -import androidx.activity.result.contract.ActivityResultContracts.TakePicture -import androidx.core.content.FileProvider -import androidx.core.widget.addTextChangedListener -import androidx.exifinterface.media.ExifInterface -import androidx.exifinterface.media.ExifInterface.ORIENTATION_UNDEFINED -import androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.IntegerOption -import com.android.identity.documenttype.StringOption -import com.android.identity.wallet.databinding.FragmentSelfSignedDetailsBinding -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.util.FormatUtil.fullDateStringToMilliseconds -import com.android.identity.wallet.util.FormatUtil.millisecondsToFullDateString -import com.android.identity.wallet.selfsigned.ProvisionInfo -import com.android.identity.wallet.selfsigned.SelfSignedDocumentData -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logError -import com.android.identity.wallet.viewmodel.SelfSignedViewModel -import com.google.android.material.datepicker.MaterialDatePicker -import java.io.File -import java.io.IOException -import java.util.Date -import kotlin.math.max -import kotlin.math.min - -class SelfSignedDetailsFragment : Fragment() { - - private val vm: SelfSignedViewModel by viewModels() - private val args: SelfSignedDetailsFragmentArgs by navArgs() - private val nameElements = listOf("given_name", "name", "gn") - - private var _binding: FragmentSelfSignedDetailsBinding? = null - private val binding get() = _binding!! - - private lateinit var provisionInfo: ProvisionInfo - private lateinit var documentNameEditText: EditText - private lateinit var holderNameEditText: EditText - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - provisionInfo = args.provisionInfo - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSelfSignedDetailsBinding.inflate(inflater) - binding.fragment = this - bindUI() - return binding.root - } - - private fun bindUI() { - // Create all fields in the screen - val documentName = getDocumentNameValue() - addField( - Field( - 0, - "Document Name", - provisionInfo.docName, - DocumentAttributeType.String, - documentName - ) - ) - documentNameEditText = binding.layoutSelfSignedDetails.findViewById(0) - vm.getFields(provisionInfo.docType).forEach { field -> - addField(field) - if (field.name in nameElements) { - holderNameEditText = binding.layoutSelfSignedDetails.findViewById(field.id) - } - } - setupTextChangeListener() - - vm.loading.observe(viewLifecycleOwner) { - binding.loadingProgress.visibility = it - } - - vm.created.observe(viewLifecycleOwner) { - Toast.makeText( - requireContext(), "Document created successfully!", - Toast.LENGTH_SHORT - ).show() - findNavController().navigate( - SelfSignedDetailsFragmentDirections.actionSelfSignedDetailsToSelectDocumentFragment() - ) - } - } - - private fun getDocumentNameValue(): String { - val value = vm.getFields(provisionInfo.docType).find { it.name in nameElements }?.value - val name = value?.toString() ?: "" - val docName = provisionInfo.docName - return if (name.isBlank()) docName else "$name's $docName" - } - - private fun setupTextChangeListener() { - holderNameEditText.addTextChangedListener { newValue -> - documentNameEditText.setText("$newValue's ${provisionInfo.docName}") - } - } - - override fun onDestroyView() { - super.onDestroyView() - updateList() - } - - private fun updateList() { - provisionInfo.docName = documentNameEditText.text.toString() - vm.getFields(provisionInfo.docType).forEachIndexed { index, field -> - vm.getFields(provisionInfo.docType)[index] = getField(field) - } - } - - private fun getField(field: Field): Field { - return when (field.fieldType) { - is DocumentAttributeType.Picture -> { - Field( - field.id, - field.label, - field.name, - field.fieldType, - getImageViewValue(field.id), - namespace = field.namespace, - parentId = field.parentId, - stringOptions = field.stringOptions, - integerOptions = field.integerOptions - ) - } - - is DocumentAttributeType.Boolean -> { - Field( - field.id, - field.label, - field.name, - field.fieldType, - getViewValue(field.id), - namespace = field.namespace, - parentId = field.parentId, - stringOptions = field.stringOptions, - integerOptions = field.integerOptions - ) - } - - else -> { - Field( - field.id, - field.label, - field.name, - field.fieldType, - getViewValue(field.id), - namespace = field.namespace, - isArray = field.isArray, - parentId = field.parentId, - stringOptions = field.stringOptions, - integerOptions = field.integerOptions - ) - } - } - } - - private fun addField(field: Field) { - when (field.fieldType) { - is DocumentAttributeType.Picture -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView(getImageView( - field.id, field.value as Bitmap - ) { dispatchTakePictureIntent(field.id) }) - } - - is DocumentAttributeType.Boolean -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - checkBox(field.id, field.value as Boolean) - ) - } - - is DocumentAttributeType.String, DocumentAttributeType.Number -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - getEditView(field.id, field.value.toString(), null) - ) - } - - is DocumentAttributeType.Date, DocumentAttributeType.DateTime -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - getEditView(field.id, field.value as String, picker(field.id, field.id + 500)) - ) - } - - is DocumentAttributeType.IntegerOptions -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - integerOptionsSpinner( - field.integerOptions!!, field.id, field.value - ) - ) - } - - is DocumentAttributeType.StringOptions -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - stringOptionsSpinner( - field.stringOptions!!, field.id, field.value - ) - ) - } - - is DocumentAttributeType.ComplexType -> { - binding.layoutSelfSignedDetails.addView( - getTitleView(field.id + 500, field.label) - ) - } - - is DocumentAttributeType.Blob -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - getEditView(field.id, field.value.toString(), null) - ) - } - } - } - - private fun getImageView( - id: Int, bitmap: Bitmap, onClickListener: View.OnClickListener? - ): View { - val imageView = ImageView(requireContext()) - imageView.id = id - imageView.setImageBitmap(bitmap) - - imageView.layoutParams = LinearLayout.LayoutParams(bitmap.width, bitmap.height).also { - it.setMargins(16, 16, 16, 0) - } - onClickListener?.let { - imageView.setOnClickListener(it) - } - return imageView - } - - private fun getTextView(id: Int, value: String): View { - val textView = TextView(requireContext()) - textView.id = id - textView.text = value - textView.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { - it.setMargins(16, 16, 16, 0) - } - return textView - } - - private fun getTitleView(id: Int, value: String): View { - val textView = TextView(requireContext()) - textView.id = id - textView.text = value - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f) - textView.setTypeface(textView.typeface, Typeface.BOLD) - textView.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { - it.setMargins(16, 32, 16, 16) - } - return textView - } - - private fun getEditView(id: Int, value: String, onClickListener: View.OnClickListener?): View { - val editText = EditText(requireContext()) - editText.id = id - editText.text = Editable.Factory.getInstance().newEditable(value) - editText.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { - it.setMargins(16, 0, 16, 0) - } - onClickListener?.let { - editText.setOnClickListener(it) - // avoid open keyboard - editText.inputType = InputType.TYPE_NULL - editText.focusable = NOT_FOCUSABLE - } - return editText - } - - fun onCreateSelfSigned() { - updateList() - val dData = SelfSignedDocumentData( - provisionInfo, vm.getFields(provisionInfo.docType) - ) - vm.createSelfSigned(dData) - binding.loadingProgress.visibility = View.VISIBLE - } - - private fun getImageViewValue(id: Int): Bitmap { - val imageView = binding.layoutSelfSignedDetails.findViewById(id) - val bitmap = Bitmap.createBitmap( - imageView.width, imageView.height, Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - imageView.draw(canvas) - return bitmap - } - - private fun getViewValue(id: Int): Any? { - return when (val view = binding.layoutSelfSignedDetails.findViewById(id)) { - is CheckBox -> { - view.isChecked - } - - is TextView -> { - view.text.toString() - } - - is Spinner -> { - when (view.selectedItem) { - is StringOption -> (view.selectedItem as StringOption).value - is IntegerOption -> (view.selectedItem as IntegerOption).value - else -> view.selectedItem.toString() - } - } - - - else -> { - String() - } - } - } - - private fun setViewValue(id: Int, value: String) { - val view = binding.layoutSelfSignedDetails.findViewById(id) - if (view is TextView) { - view.text = value - } - } - - /** - * OnClickListener for date picker - */ - private fun picker(id: Int, idLabel: Int) = View.OnClickListener { - val titleText = getViewValue(idLabel) as String - val dateText = getViewValue(id) as String - log("$dateText - ${fullDateStringToMilliseconds(dateText)}") - val datePicker = MaterialDatePicker.Builder.datePicker().setTitleText(titleText) - .setSelection(fullDateStringToMilliseconds(dateText)).build() - datePicker.addOnPositiveButtonClickListener { - log("$it - ${millisecondsToFullDateString(it)}") - setViewValue(id, millisecondsToFullDateString(it)) - } - datePicker.show(parentFragmentManager, view?.tag?.toString()) - } - - private fun stringOptionsSpinner( - options: List, id: Int, value: Any? - ): View { - val spinner = Spinner(context) - spinner.id = id - val adapter = - ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, options) - spinner.adapter = adapter - val selected = options.find { (it.value == null && value == null) || it.value == value } - if (selected != null) { - spinner.setSelection(options.indexOf(selected)) - } - return spinner - } - - private fun integerOptionsSpinner( - options: List, id: Int, value: Any? - ): View { - val spinner = Spinner(context) - spinner.id = id - val adapter = - ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, options) - spinner.adapter = adapter - val selected = options.find { (it.value == null && value == null) || it.value == value } - if (selected != null) { - spinner.setSelection(options.indexOf(selected)) - } - return spinner - } - - private fun checkBox(id: Int, value: Boolean): View { - val checkBox = CheckBox(context) - checkBox.id = id - checkBox.isChecked = value - return checkBox - } - - // Following to enable take picture - private lateinit var photoUri: Uri - private lateinit var currentPhotoPath: String - private var imageViewId: Int? = null - private val takePicture = registerForActivityResult(TakePicture()) { isSuccess -> - if (isSuccess) { - val rotation = calculateDegrees() - setPic(rotation) - } - } - private val cameraLauncher = registerForActivityResult(RequestPermission()) { granted -> - if (granted) { - proceedTakingPhoto() - } - } - - private fun dispatchTakePictureIntent(viewId: Int) { - imageViewId = viewId - if (!hasCameraAvailable()) return - if (!canTakePhoto()) return - cameraLauncher.launch(Manifest.permission.CAMERA) - } - - private fun proceedTakingPhoto() { - try { - val imageFile = createImageFile() - photoUri = getUriForFile(imageFile) - takePicture.launch(photoUri) - } catch (exception: IOException) { - log("Error capturing image", exception) - } - } - - private fun hasCameraAvailable(): Boolean { - val packageManager = requireContext().packageManager - if (!packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { - val errorMessage = "This device does not have a camera." - Toast.makeText(activity, errorMessage, Toast.LENGTH_SHORT).show() - return false - } - return true - } - - private fun canTakePhoto(): Boolean { - val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - // Ensure that there's a camera activity to handle the intent - if (takePictureIntent.resolveActivity(requireContext().packageManager) == null) { - val errorMessage = "Could not find camera activity." - Toast.makeText(activity, errorMessage, Toast.LENGTH_SHORT).show() - return false - } - return true - } - - @Throws(IOException::class) - private fun createImageFile(): File { - // Create an image file name - val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) - val storageDir: File? = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES) - return File.createTempFile( - "JPEG_${timeStamp}_", /* prefix */ - ".jpg", /* suffix */ - storageDir /* directory */ - ).apply { - currentPhotoPath = absolutePath - } - } - - private fun getUriForFile(file: File): Uri { - val authority = "${requireContext().packageName}.fileprovider" - return FileProvider.getUriForFile(requireContext(), authority, file) - } - - private fun calculateDegrees(): Float { - val inputStream = requireContext().contentResolver.openInputStream(photoUri) - val exifInterface = ExifInterface(inputStream!!) - return when (exifInterface.getAttributeInt(TAG_ORIENTATION, ORIENTATION_UNDEFINED)) { - ExifInterface.ORIENTATION_ROTATE_90 -> 90f - ExifInterface.ORIENTATION_ROTATE_180 -> 180f - ExifInterface.ORIENTATION_ROTATE_270 -> 270f - else -> 0f - }.apply { - inputStream.close() - } - } - - private fun setPic(rotation: Float) { - val id = imageViewId - if (id == null) { - logError("No image view id, impossible to set picture") - return - } - - val imageView = binding.layoutSelfSignedDetails.findViewById(id) - - // Get the dimensions of the View - val targetW: Int = imageView.width - val targetH: Int = imageView.height - - val bmOptions = BitmapFactory.Options().apply { - // Get the dimensions of the bitmap - inJustDecodeBounds = true - - val photoW: Int = outWidth - val photoH: Int = outHeight - - // Determine how much to scale down the image - val scaleFactor: Int = max(1, min(photoW / targetW, photoH / targetH)) - - // Decode the image file into a Bitmap sized to fill the View - inJustDecodeBounds = false - inSampleSize = scaleFactor - inPurgeable = true - } - - val original = BitmapFactory.decodeFile(currentPhotoPath, bmOptions) - val rotated = if (rotation != 0f) { - val matrix = Matrix() - matrix.postRotate(rotation) - Bitmap.createBitmap(original, 0, 0, original.width, original.height, matrix, true) - } else original - imageView.setImageBitmap(rotated) - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt deleted file mode 100644 index 9f45bf667..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.databinding.FragmentShareDocumentBinding -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel - -class ShareDocumentFragment : Fragment() { - - private val viewModel: ShareDocumentViewModel by viewModels() - - private var _binding: FragmentShareDocumentBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentShareDocumentBinding.inflate(inflater) - binding.vm = viewModel - binding.fragment = this - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackCallback) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewModel.message.set("Scan QR code with mdoc verifier device") - viewModel.getTransferStatus().observe(viewLifecycleOwner) { - when (it) { - TransferStatus.CONNECTED -> { - viewModel.message.set("Connected!") - val destination = ShareDocumentFragmentDirections.toTransferDocumentFragment() - findNavController().navigate(destination) - } - - TransferStatus.REQUEST -> { - viewModel.message.set("Request received!") - } - - TransferStatus.DISCONNECTED -> { - viewModel.message.set("Disconnected!") - findNavController().navigateUp() - } - - TransferStatus.ERROR -> { - viewModel.message.set("Error on presentation!") - } - - TransferStatus.ENGAGEMENT_DETECTED -> { - viewModel.message.set("Engagement detected!") - } - - TransferStatus.CONNECTING -> { - viewModel.message.set("Connecting...") - } - - else -> {} - } - } - } - - override fun onResume() { - super.onResume() - viewModel.triggerQrEngagement() - viewModel.showQrCode() - } - - private val onBackCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - onCancel() - } - } - - fun onCancel() { - viewModel.cancelPresentation() - findNavController().navigateUp() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt deleted file mode 100644 index e12b6fb29..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.R -import com.android.identity.wallet.databinding.FragmentTransferDocumentBinding -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.transfer.TransferManager -import com.android.identity.wallet.trustmanagement.CustomValidators -import com.android.identity.wallet.trustmanagement.getCommonName -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.log -import com.android.identity.wallet.viewmodel.TransferDocumentViewModel -import java.security.cert.X509Certificate - -class TransferDocumentFragment : Fragment() { - private var _binding: FragmentTransferDocumentBinding? = null - private val binding get() = _binding!! - - private val viewModel: TransferDocumentViewModel by activityViewModels() - - override fun onAttach(context: Context) { - super.onAttach(context) - val backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - onDone() - } - } - requireActivity().onBackPressedDispatcher.addCallback(this, backPressedCallback) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentTransferDocumentBinding.inflate(inflater) - binding.fragment = this - binding.vm = viewModel - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - viewModel.getTransferStatus().observe(viewLifecycleOwner) { transferStatus -> - when (transferStatus) { - TransferStatus.CONNECTED -> log("Connected") - TransferStatus.REQUEST -> onTransferRequested() - TransferStatus.REQUEST_SERVED -> onRequestServed() - TransferStatus.DISCONNECTED -> onTransferDisconnected() - TransferStatus.ERROR -> onTransferError() - else -> {} - } - } - viewModel.connectionClosedLiveData.observe(viewLifecycleOwner) { - onCloseConnection( - sendSessionTerminationMessage = true, - useTransportSpecificSessionTermination = false - ) - } - viewModel.authConfirmationState.observe(viewLifecycleOwner) { cancelled -> - if (cancelled == true) { - viewModel.onAuthenticationCancellationConsumed() - onDone() - findNavController().navigateUp() - } - } - } - - private fun onRequestServed() { - log("Request Served") - } - - private fun onTransferRequested() { - log("Request") - var commonName = "" - var trusted = false - try { - val requestedDocuments = viewModel.getRequestedDocuments() - requestedDocuments.forEach { reqDoc -> - val docs = viewModel.getDocuments().filter { reqDoc.docType == it.docType } - if (!viewModel.getSelectedDocuments().any { reqDoc.docType == it.docType }) { - if (docs.isEmpty()) { - binding.txtDocuments.append("- No document found for ${reqDoc.docType}\n") - return@forEach - } else if (docs.size == 1) { - viewModel.getSelectedDocuments().add(docs[0]) - } else { - showDocumentSelection(docs) - return - } - } - val doc = viewModel.getSelectedDocuments().first { reqDoc.docType == it.docType } - if (reqDoc.readerAuth != null && reqDoc.readerAuthenticated) { - var certChain: List = - reqDoc.readerCertificateChain!!.certificates.map { it.javaX509Certificate } - .toList() - val customValidators = CustomValidators.getByDocType(doc.docType) - val result = HolderApp.trustManagerInstance.verify( - chain = certChain, - customValidators = customValidators - ) - trusted = result.isTrusted - if (result.trustChain.any()) { - certChain = result.trustChain - } - commonName = certChain.last().issuerX500Principal.getCommonName("") - - // Add some information about the reader certificate used - if (result.isTrusted) { - binding.txtDocuments.append("- Trusted reader auth used: ($commonName)\n") - } else { - binding.txtDocuments.append("- Not trusted reader auth used: ($commonName)\n") - if (result.error != null) { - binding.txtDocuments.append("- TrustManager Error: (${result.error})\n") - } - } - } - binding.txtDocuments.append("- ${doc.userVisibleName} (${doc.docType})\n") - } - if (viewModel.getSelectedDocuments().isNotEmpty()) { - viewModel.createSelectedItemsList() - val direction = TransferDocumentFragmentDirections - .navigateToConfirmation(commonName, trusted) - findNavController().navigate(direction) - } else { - // Send response with 0 documents - viewModel.sendResponseForSelection( - onResultReady = { - } - ) - } - // TODO: this is kind of a hack but we really need to move the sending of the - // message to here instead of in the auth confirmation dialog - if (PreferencesHelper.isConnectionAutoCloseEnabled()) { - hideButtons() - } - } catch (e: Exception) { - val message = "On request received error: ${e.message}" - log(message, e) - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() - binding.txtConnectionStatus.append("\n$message") - } - } - - private fun showDocumentSelection(doc: List) { - val alertDialogBuilder = AlertDialog.Builder(requireContext()) - alertDialogBuilder.setTitle("Select which document to share") - val listItems = doc.map { it.userVisibleName }.toTypedArray() - alertDialogBuilder.setSingleChoiceItems(listItems, -1) { dialogInterface, i -> - viewModel.getSelectedDocuments().add(doc[i]) - onTransferRequested() - dialogInterface.dismiss() - } - val mDialog = alertDialogBuilder.create() - mDialog.show() - } - - private fun onTransferDisconnected() { - log("Disconnected") - hideButtons() - TransferManager.getInstance(requireContext()).disconnect() - } - - private fun onTransferError() { - Toast.makeText(requireContext(), "An error occurred.", Toast.LENGTH_SHORT).show() - hideButtons() - TransferManager.getInstance(requireContext()).disconnect() - } - - private fun hideButtons() { - binding.txtConnectionStatus.text = getString(R.string.connection_mdoc_closed) - binding.btCloseConnection.visibility = View.GONE - binding.btCloseTerminationMessage.visibility = View.GONE - binding.btCloseTransportSpecific.visibility = View.GONE - binding.btOk.visibility = View.VISIBLE - } - - fun onCloseConnection( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - viewModel.cancelPresentation( - sendSessionTerminationMessage, - useTransportSpecificSessionTermination - ) - hideButtons() - } - - fun onDone() { - onCloseConnection( - sendSessionTerminationMessage = true, - useTransportSpecificSessionTermination = false - ) - findNavController().navigateUp() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt deleted file mode 100644 index b0588e39b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt +++ /dev/null @@ -1,424 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.CounterInput -import com.android.identity.wallet.composables.DropDownIndicator -import com.android.identity.wallet.composables.LabeledUserInput -import com.android.identity.wallet.composables.OutlinedContainerHorizontal -import com.android.identity.wallet.composables.TextDropDownRow -import com.android.identity.wallet.composables.ValueLabel -import com.android.identity.wallet.composables.gradientFor -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.support.SecureAreaSupport -import com.android.identity.wallet.support.SecureAreaSupportState -import com.android.identity.wallet.support.toSecureAreaState -import com.android.identity.wallet.util.ProvisioningUtil - -@Composable -fun AddSelfSignedDocumentScreen( - viewModel: AddSelfSignedViewModel, - onNext: () -> Unit -) { - val screenState by viewModel.screenState.collectAsState() - - AddSelfSignedDocumentScreenContent( - modifier = Modifier.fillMaxSize(), - screenState = screenState, - documentItems = viewModel.documentItems, - onDocumentTypeChanged = viewModel::updateDocumentType, - onCardArtSelected = viewModel::updateCardArt, - onDocumentNameChanged = viewModel::updateDocumentName, - onKeystoreImplementationChanged = viewModel::updateKeystoreImplementation, - onSecureAreaSupportStateUpdated = viewModel::updateSecureAreaSupportState, - onNumberOfMsoChanged = viewModel::updateNumberOfMso, - onMaxUseOfMsoChanged = viewModel::updateMaxUseOfMso, - onValidityInDaysChanged = viewModel::updateValidityInDays, - onMinValidityInDaysChanged = viewModel::updateMinValidityInDays, - onNext = onNext - ) -} - -@Composable -private fun AddSelfSignedDocumentScreenContent( - modifier: Modifier, - screenState: AddSelfSignedScreenState, - documentItems: List, - onDocumentTypeChanged: (newType: String, newName: String) -> Unit, - onCardArtSelected: (newCardArt: DocumentColor) -> Unit, - onDocumentNameChanged: (newValue: String) -> Unit, - onKeystoreImplementationChanged: (newImplementation: CurrentSecureArea) -> Unit, - onSecureAreaSupportStateUpdated: (newState: SecureAreaSupportState) -> Unit, - onNumberOfMsoChanged: (newValue: Int) -> Unit, - onMaxUseOfMsoChanged: (newValue: Int) -> Unit, - onValidityInDaysChanged: (newValue: Int) -> Unit, - onMinValidityInDaysChanged: (newValue: Int) -> Unit, - onNext: () -> Unit -) { - Scaffold(modifier = modifier) { paddingValues -> - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .padding(paddingValues) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.fillMaxWidth()) - DocumentTypeChooser( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - documentItems = documentItems, - currentDocumentType = screenState.documentType, - onDocumentTypeSelected = onDocumentTypeChanged - ) - CardArtChooser( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - currentCardArt = screenState.cardArt, - onCardArtSelected = onCardArtSelected - ) - DocumentNameInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - value = screenState.documentName, - onValueChanged = onDocumentNameChanged - ) - KeystoreImplementationChooser( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - currentImplementation = screenState.currentSecureArea, - onKeystoreImplementationChanged = onKeystoreImplementationChanged - ) - SecureAreaSupport.getInstance( - LocalContext.current, - screenState.currentSecureArea - ).SecureAreaAuthUi(onUiStateUpdated = onSecureAreaSupportStateUpdated) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.txt_number_mso), - value = screenState.numberOfMso, - onValueChange = onNumberOfMsoChanged - ) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.txt_max_use_mso), - value = screenState.maxUseOfMso, - onValueChange = onMaxUseOfMsoChanged - ) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.validity_in_days), - value = screenState.validityInDays, - onValueChange = onValidityInDaysChanged - ) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.minimum_validity_in_days), - value = screenState.minValidityInDays, - onValueChange = onMinValidityInDaysChanged - ) - Button( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - onClick = onNext - ) { - Text(text = "Next") - } - } - } -} - -@Composable -private fun DocumentTypeChooser( - modifier: Modifier = Modifier, - documentItems: List, - currentDocumentType: String, - onDocumentTypeSelected: (newType: String, newName: String) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_document_type) - ) { - var expanded by remember { mutableStateOf(false) } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true } - ) { - documentItems.find {it.docType == currentDocumentType}?.let { - ValueLabel( - modifier = Modifier.weight(1f), - label = it.displayName - ) - } - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier - .fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - documentItems.forEach{ - TextDropDownRow( - label = it.displayName, - onSelected = { - onDocumentTypeSelected(it.docType, it.displayName) - expanded = false - } - ) - } - } - } -} - -@Composable -private fun CardArtChooser( - modifier: Modifier, - currentCardArt: DocumentColor, - onCardArtSelected: (newCardArt: DocumentColor) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_card_art) - ) { - var expanded by remember { mutableStateOf(false) } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true }, - outlineBrush = gradientFor(currentCardArt) - ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .background(gradientFor(currentCardArt), RoundedCornerShape(8.dp)), - ) - ValueLabel( - modifier = Modifier - .padding(horizontal = 12.dp) - .weight(1f), - label = stringResource(id = colorNameFor(currentCardArt)) - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier - .fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - CardArtDropDownRow( - cardArt = DocumentColor.Green, - onSelected = { - onCardArtSelected(DocumentColor.Green) - expanded = false - } - ) - CardArtDropDownRow( - cardArt = DocumentColor.Yellow, - onSelected = { - onCardArtSelected(DocumentColor.Yellow) - expanded = false - } - ) - CardArtDropDownRow( - cardArt = DocumentColor.Blue, - onSelected = { - onCardArtSelected(DocumentColor.Blue) - expanded = false - } - ) - CardArtDropDownRow( - cardArt = DocumentColor.Red, - onSelected = { - onCardArtSelected(DocumentColor.Red) - expanded = false - } - ) - } - } -} - -@Composable -private fun DocumentNameInput( - modifier: Modifier = Modifier, - value: String, - onValueChanged: (newValue: String) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_document_name) - ) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - textStyle = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - value = value, - onValueChange = onValueChanged, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) - ) - } - } -} - -@Composable -private fun KeystoreImplementationChooser( - modifier: Modifier = Modifier, - currentImplementation: CurrentSecureArea, - onKeystoreImplementationChanged: (newImplementation: CurrentSecureArea) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_keystore_implementation) - ) { - var expanded by remember { mutableStateOf(false) } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true } - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = currentImplementation.displayName - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier - .fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - ProvisioningUtil.getInstance(LocalContext.current) - .secureAreaRepository.implementations.forEach { implementation -> - TextDropDownRow( - label = implementation.displayName, - onSelected = { - onKeystoreImplementationChanged(implementation.toSecureAreaState()) - expanded = false - } - ) - } - } - } -} - -@Composable -private fun CardArtDropDownRow( - modifier: Modifier = Modifier, - cardArt: DocumentColor, - onSelected: () -> Unit -) { - DropdownMenuItem( - text = { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .background(gradientFor(cardArt), RoundedCornerShape(8.dp)), - ) - ValueLabel(label = stringResource(id = colorNameFor(cardArt))) - } - }, - onClick = onSelected - ) -} - -@Composable -fun OutlinedContainerVertical( - modifier: Modifier = Modifier, - outlineBorderWidth: Dp = 2.dp, - outlineBrush: Brush? = null, - content: @Composable ColumnScope.() -> Unit -) { - val brush = outlineBrush ?: SolidColor(MaterialTheme.colorScheme.outline) - Row( - modifier = modifier - .heightIn(48.dp) - .clip(RoundedCornerShape(12.dp)) - .border(outlineBorderWidth, brush, RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.inverseOnSurface), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - content() - } - } -} - -@StringRes -private fun colorNameFor(cardArt: DocumentColor): Int { - return when (cardArt) { - is DocumentColor.Green -> R.string.document_color_green - is DocumentColor.Yellow -> R.string.document_color_yellow - is DocumentColor.Blue -> R.string.document_color_blue - is DocumentColor.Red -> R.string.document_color_red - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt deleted file mode 100644 index b642f0a14..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.theme.HolderAppTheme - -class AddSelfSignedFragment : Fragment() { - - private val viewModel: AddSelfSignedViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - AddSelfSignedDocumentScreen( - viewModel = viewModel, - onNext = { onNext() } - ) - } - } - } - } - - private fun onNext() { - val state = viewModel.screenState.value - val secureAreaScreenState = requireNotNull(state.secureAreaSupportState) - val provisionInfo = ProvisionInfo( - docType = state.documentType, - docName = state.documentName, - docColor = state.cardArt.value, - currentSecureArea = state.currentSecureArea, - secureAreaSupportState = secureAreaScreenState, - validityInDays = state.validityInDays, - minValidityInDays = state.minValidityInDays, - numberMso = state.numberOfMso, - maxUseMso = state.maxUseOfMso - ) - val destination = AddSelfSignedFragmentDirections - .actionAddSelfSignedToSelfSignedDetails(provisionInfo) - findNavController().navigate(destination) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt deleted file mode 100644 index 84e36cc1b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import android.os.Parcelable -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.support.SecureAreaSupportState -import com.android.identity.wallet.support.toSecureAreaState -import com.android.identity.wallet.util.DocumentData -import com.android.identity.wallet.util.ProvisioningUtil -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AddSelfSignedScreenState( - val documentType: String = DocumentData.MDL_DOCTYPE, - val cardArt: DocumentColor = DocumentColor.Green, - val documentName: String = "Driving License", - val currentSecureArea: CurrentSecureArea = ProvisioningUtil.defaultSecureArea.toSecureAreaState(), - val numberOfMso: Int = 3, - val maxUseOfMso: Int = 1, - val validityInDays: Int = 30, - val minValidityInDays: Int = 10, - val secureAreaSupportState: SecureAreaSupportState? = null, -) : Parcelable 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 deleted file mode 100644 index fdc52d65a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.support.SecureAreaSupportState -import com.android.identity.wallet.util.getState -import com.android.identity.wallet.util.updateState -import kotlinx.coroutines.flow.StateFlow -import java.lang.Integer.max - -class AddSelfSignedViewModel( - private val savedStateHandle: SavedStateHandle -) : ViewModel() { - - val screenState: StateFlow = savedStateHandle.getState( - AddSelfSignedScreenState() - ) - - val documentItems: List = - HolderApp.documentTypeRepositoryInstance.documentTypes.filter { it.mdocDocumentType != null } - .map { DocumentItem(it.mdocDocumentType!!.docType, it.displayName) } - - - fun updateDocumentType(newValue: String, newName: String) { - savedStateHandle.updateState { - it.copy(documentType = newValue, documentName = newName) - } - } - - fun updateCardArt(newValue: DocumentColor) { - savedStateHandle.updateState { - it.copy(cardArt = newValue) - } - } - - fun updateDocumentName(newValue: String) { - savedStateHandle.updateState { - it.copy(documentName = newValue) - } - } - - fun updateKeystoreImplementation(newValue: CurrentSecureArea) { - savedStateHandle.updateState { - it.copy(currentSecureArea = newValue) - } - } - - fun updateSecureAreaSupportState(newValue: SecureAreaSupportState) { - savedStateHandle.updateState { - it.copy(secureAreaSupportState = newValue) - } - } - - fun updateValidityInDays(newValue: Int) { - val state = savedStateHandle.getState(AddSelfSignedScreenState()) - if (newValue < state.value.minValidityInDays) return - savedStateHandle.updateState { - it.copy(validityInDays = newValue) - } - } - - fun updateMinValidityInDays(newValue: Int) { - if (newValue <= 0) return - savedStateHandle.updateState { - val validityDays = max(newValue, it.validityInDays) - it.copy(minValidityInDays = newValue, validityInDays = validityDays) - } - } - - - fun updateNumberOfMso(newValue: Int) { - if (newValue <= 0) return - savedStateHandle.updateState { - it.copy(numberOfMso = newValue) - } - } - - fun updateMaxUseOfMso(newValue: Int) { - if (newValue <= 0) return - savedStateHandle.updateState { - it.copy(maxUseOfMso = newValue) - } - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt deleted file mode 100644 index 1009df77f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.identity.wallet.selfsigned - -data class DocumentItem ( - val docType: String, - val displayName: String -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt deleted file mode 100644 index 80e34352f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import android.os.Parcelable -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.support.SecureAreaSupportState -import kotlinx.parcelize.Parcelize - -data class SelfSignedDocumentData( - val provisionInfo: ProvisionInfo, - val fields: List -) - -@Parcelize -data class ProvisionInfo( - val docType: String, - var docName: String, - var docColor: Int, - val currentSecureArea: CurrentSecureArea, - val secureAreaSupportState: SecureAreaSupportState, - val validityInDays: Int, - val minValidityInDays: Int, - val numberMso: Int, - val maxUseMso: Int -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt deleted file mode 100644 index 75ab58dc8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.android.identity.wallet.settings - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.theme.HolderAppTheme - -class CaCertificateDetailsFragment : Fragment() { - private val viewModel: CaCertificatesViewModel by activityViewModels { - CaCertificatesViewModel.factory() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - val state by viewModel.currentCertificateItem.collectAsState() - HolderAppTheme { - CaCertificateDetailsScreen(certificateItem = state, - onDeleteCertificate = { deleteCertificate() }) - } - } - } - } - - private fun deleteCertificate() { - viewModel.deleteCertificate() - viewModel.loadCertificates() - findNavController().popBackStack() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt deleted file mode 100644 index 03ad6e22f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.theme.HolderAppTheme -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.Date - -@Composable -fun CaCertificateDetailsScreen( - certificateItem: CertificateItem?, - onDeleteCertificate: () -> Unit = {} -) { - if (certificateItem == null) { - Title(title = "No certificate provided") - } else { - val scrollState = rememberScrollState() - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Column( - modifier = Modifier - .verticalScroll(scrollState) - .weight(1f) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Title(title = certificateItem.title) - Subtitle(title = "Issued to") - Line( - modifier = Modifier, - text = "Common Name (CN) " + certificateItem.commonNameSubject - ) - Line( - modifier = Modifier, - text = "Organisation (O) " + certificateItem.organisationSubject - ) - Line( - modifier = Modifier, - text = "Organisational Unit (OU) " + certificateItem.organisationalUnitSubject - ) - Subtitle(title = "Issued by") - Line( - modifier = Modifier, - text = "Common Name (CN) " + certificateItem.commonNameIssuer - ) - Line( - modifier = Modifier, - text = "Organisation (O) " + certificateItem.organisationIssuer - ) - Line( - modifier = Modifier, - text = "Organisational Unit (OU) " + certificateItem.organisationalUnitIssuer - ) - Subtitle(title = "Fingerprints") - Line(modifier = Modifier, "SHA-256 fingerprint") - Line(modifier = Modifier.padding(16.dp), certificateItem.sha255Fingerprint) - Line(modifier = Modifier, "SHA-1 fingerprint") - Line(modifier = Modifier.padding(16.dp), certificateItem.sha1Fingerprint) - if (certificateItem.docTypes.isNotEmpty()){ - Subtitle(title = "Supported mdoc types") - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 0.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(certificateItem.docTypes) { docType -> - Line(modifier = Modifier, text = docType) - } - } - } - } - if (certificateItem.supportsDelete) { - Button(onClick = onDeleteCertificate) { - Text(text = "Delete") - } - } - } - } -} - -@Composable -fun Title(title: String) { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -fun Subtitle(title: String) { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -fun Line(modifier: Modifier, text: String) { - Text( - modifier = modifier.fillMaxWidth(), - text = text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewCaCertificatesScreen() { - HolderAppTheme { - CaCertificateDetailsScreen( - certificateItem = CertificateItem( - title = "Test 1", - commonNameSubject = "*.google.com", - organisationSubject = "", - organisationalUnitSubject = "", - commonNameIssuer = "GTS CA 1C3", - organisationIssuer = "Google Trust Services LLC", - organisationalUnitIssuer = "", - notBefore = Date.from(LocalDateTime.now().minusDays(365).toInstant(ZoneOffset.UTC)), - notAfter = Date.from(LocalDateTime.now().plusDays(365).toInstant(ZoneOffset.UTC)), - sha255Fingerprint = "03 5C 31 E7 A9 F3 71 2B 27 1C 5A 8D 82 E5 6C 5B 92 BC FC 28 7F72D7 4A B6 9D 61 BF 53 EF 3E 67", - sha1Fingerprint = "9D 80 9B CF 63 AA86 29 E9 3C 78 9A EA DA 15 56 7E BF 56 D8", - docTypes = listOf("Doc type 1", "Doc type 2"), - supportsDelete = true, - trustPoint = null - ), - onDeleteCertificate = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt deleted file mode 100644 index 06b201530..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.ClipboardManager -import android.content.Context -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.crypto.X509Cert -import com.android.identity.trustmanagement.TrustPoint -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.theme.HolderAppTheme -import com.android.identity.wallet.trustmanagement.getSubjectKeyIdentifier -import com.google.android.material.R -import com.google.android.material.snackbar.Snackbar -import java.io.ByteArrayInputStream -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate - - -class CaCertificatesFragment : Fragment() { - - private val viewModel: CaCertificatesViewModel by activityViewModels { - CaCertificatesViewModel.factory() - } - - private val browseCertificateLauncher = - registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> - uris.forEach { uri -> importCertificate(uri) } - viewModel.loadCertificates() - } - - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - val state = viewModel.screenState.collectAsState().value - viewModel.loadCertificates() - HolderAppTheme { - CaCertificatesScreen( - screenState = state, - onSelectCertificate = { - viewModel.setCurrentCertificateItem(it) - openDetails() - }, - onImportCertificate = { fileDialog() }, - onPasteCertificate = { pasteCertificate() } - ) - } - } - } - } - - private fun openDetails() { - val destination = CaCertificatesFragmentDirections.toCaCertificateDetails() - findNavController().navigate(destination) - } - - private fun fileDialog() { - browseCertificateLauncher.launch(arrayOf("*/*")) - } - - private fun importCertificate(uri: Uri) { - try { - this.requireContext().contentResolver.openInputStream(uri).use { inputStream -> - if (inputStream != null) { - val certificate = parseCertificate(inputStream.readBytes()) - HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded))) - HolderApp.certificateStorageEngineInstance.put( - certificate.getSubjectKeyIdentifier(), - certificate.encoded - ) - } - } - } catch (e: Throwable) { - showException(e) - } - } - - private fun pasteCertificate() { - try { - val clipboard = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - if (!clipboard.hasPrimaryClip() - || clipboard.primaryClip?.itemCount == 0 - || clipboard.primaryClip?.getItemAt(0)?.text == null - ) { - showMessage("Nothing found to paste") - return - } - val text = clipboard.primaryClip?.getItemAt(0)?.text!! - val certificate = parseCertificate(text.toString().toByteArray()) - HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded))) - HolderApp.certificateStorageEngineInstance.put( - certificate.getSubjectKeyIdentifier(), - certificate.encoded - ) - } catch (e: Throwable) { - showException(e) - } finally { - viewModel.loadCertificates() - } - } - - private fun showException(exception: Throwable) { - val message = when (exception) { - is FileAlreadyExistsException -> "The certificate is already in the mDoc Issuer Trust Store" - is CertificateException -> "The certificate could not be parsed correctly" - else -> exception.message - } - showMessage(message.toString()) - } - - private fun showMessage(message: String) { - val snackbar = Snackbar.make( - this.requireView(), - message, - Snackbar.LENGTH_LONG - ) - val snackTextView = snackbar.view.findViewById(R.id.snackbar_text) as TextView - snackTextView.maxLines = 4 - snackbar.show() - } - - /** - * Parse a byte array an X509 certificate - */ - private fun parseCertificate(certificateBytes: ByteArray): X509Certificate { - return CertificateFactory.getInstance("X509") - .generateCertificate(ByteArrayInputStream(certificateBytes)) as X509Certificate - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt deleted file mode 100644 index 863176bfa..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.theme.HolderAppTheme -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.Date - -@Composable -fun CaCertificatesScreen( - screenState: CaCertificatesScreenState, - onSelectCertificate: (item: CertificateItem) -> Unit, - onImportCertificate: () -> Unit, - onPasteCertificate: () -> Unit -) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .verticalScroll(scrollState) - .weight(1f) - ) { - if (screenState.certificates.isEmpty()) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = "No certificates provided", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(screenState.certificates) { certificateItem -> - Text( - modifier = Modifier.clickable { onSelectCertificate(certificateItem) }, - text = certificateItem.title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } - } - Button(onClick = onImportCertificate) { - Text(text = "Import") - } - Button(onClick = onPasteCertificate) { - Text(text = "Paste") - } - } -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewCaCertificatesScreen() { - HolderAppTheme { - CaCertificatesScreen( - screenState = CaCertificatesScreenState( - listOf( - CertificateItem( - title = "Test 1", - commonNameSubject = "*.google.com", - organisationSubject = "", - organisationalUnitSubject = "", - commonNameIssuer = "GTS CA 1C3", - organisationIssuer = "Google Trust Services LLC", - organisationalUnitIssuer = "", - notBefore = Date.from( - LocalDateTime.now().minusDays(365).toInstant(ZoneOffset.UTC) - ), - notAfter = Date.from( - LocalDateTime.now().plusDays(365).toInstant(ZoneOffset.UTC) - ), - sha255Fingerprint = "03 5C 31 E7 A9 F3 71 2B 27 1C 5A 8D 82 E5 6C 5B 92 BC FC 28 7F72D7 4A B6 9D 61 BF 53 EF 3E 67", - sha1Fingerprint = "9D 80 9B CF 63 AA86 29 E9 3C 78 9A EA DA 15 56 7E BF 56 D8", - docTypes = emptyList(), - supportsDelete = false, - trustPoint = null - ), - CertificateItem( - title = "Test 2", - commonNameSubject = "*.google.com", - organisationSubject = "", - organisationalUnitSubject = "", - commonNameIssuer = "GTS CA 1C3", - organisationIssuer = "Google Trust Services LLC", - organisationalUnitIssuer = "", - notBefore = Date.from( - LocalDateTime.now().minusDays(100).toInstant( - ZoneOffset.UTC - ) - ), - notAfter = Date.from( - LocalDateTime.now().plusDays(100).toInstant(ZoneOffset.UTC) - ), - sha255Fingerprint = "03 5C 31 E7 A9 F3 71 2B 27 1C 5A 8D 82 E5 6C 5B 92 BC FC 28 7F72D7 4A B6 9D 61 BF 53 EF 3E 67", - sha1Fingerprint = "9D 80 9B CF 63 AA86 29 E9 3C 78 9A EA DA 15 56 7E BF 56 D8", - docTypes = emptyList(), - supportsDelete = false, - trustPoint = null - ) - ) - ), - onSelectCertificate = {}, - onImportCertificate = {}, - onPasteCertificate = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt deleted file mode 100644 index b6b8cc21e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.identity.wallet.settings - -data class CaCertificatesScreenState ( - val certificates: List = emptyList() -) { - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt deleted file mode 100644 index abffacc97..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.android.identity.wallet.settings - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.trustmanagement.getSubjectKeyIdentifier -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -class CaCertificatesViewModel() : ViewModel() { - - private val _screenState = MutableStateFlow(CaCertificatesScreenState()) - val screenState: StateFlow = _screenState.asStateFlow() - - private val _currentCertificateItem = MutableStateFlow(null) - val currentCertificateItem = _currentCertificateItem.asStateFlow() - fun loadCertificates() { - val certificates = - HolderApp.trustManagerInstance.getAllTrustPoints().map { it.toCertificateItem() } - _screenState.update { it.copy(certificates = certificates) } - } - - fun setCurrentCertificateItem(certificateItem: CertificateItem) { - _currentCertificateItem.update { certificateItem } - } - - fun deleteCertificate() { - _currentCertificateItem.value?.trustPoint?.let { - HolderApp.trustManagerInstance.removeTrustPoint(it) - HolderApp.certificateStorageEngineInstance.delete( - it.certificate.javaX509Certificate.getSubjectKeyIdentifier() - ) - } - } - - companion object { - fun factory(): ViewModelProvider.Factory { - return viewModelFactory { - initializer { CaCertificatesViewModel() } - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt deleted file mode 100644 index e7d9b1de9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.identity.wallet.settings - -import com.android.identity.trustmanagement.TrustPoint -import java.util.Date - -data class CertificateItem( - val title: String, - val commonNameSubject: String, - val organisationSubject: String, - val organisationalUnitSubject: String, - val commonNameIssuer: String, - val organisationIssuer: String, - val organisationalUnitIssuer: String, - val notBefore: Date, - val notAfter: Date, - val sha255Fingerprint: String, - val sha1Fingerprint: String, - val docTypes: List, - val supportsDelete: Boolean, - val trustPoint: TrustPoint? -) { -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt b/appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt deleted file mode 100644 index ed12ad0b5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.android.identity.wallet.settings - -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.trustmanagement.TrustPoint -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.trustmanagement.getCommonName -import com.android.identity.wallet.trustmanagement.getOrganisation -import com.android.identity.wallet.trustmanagement.getSubjectKeyIdentifier -import com.android.identity.wallet.trustmanagement.organisationalUnit -import java.lang.StringBuilder -import java.security.MessageDigest - -fun TrustPoint.toCertificateItem(docTypes: List = emptyList()): CertificateItem { - val subject = this.certificate.javaX509Certificate.subjectX500Principal - val issuer = this.certificate.javaX509Certificate.issuerX500Principal - val sha255Fingerprint = hexWithSpaces( - MessageDigest.getInstance("SHA-256").digest( - this.certificate.encodedCertificate - ) - ) - val sha1Fingerprint = hexWithSpaces( - MessageDigest.getInstance("SHA-1").digest( - this.certificate.encodedCertificate - ) - ) - val defaultValue = "" - - return CertificateItem( - title = subject.name, - commonNameSubject = subject.getCommonName(defaultValue), - organisationSubject = subject.getOrganisation(defaultValue), - organisationalUnitSubject = subject.organisationalUnit(defaultValue), - commonNameIssuer = issuer.getCommonName(defaultValue), - organisationIssuer = issuer.getOrganisation(defaultValue), - organisationalUnitIssuer = issuer.organisationalUnit(defaultValue), - notBefore = this.certificate.javaX509Certificate.notBefore, - notAfter = this.certificate.javaX509Certificate.notAfter, - sha255Fingerprint = sha255Fingerprint, - sha1Fingerprint = sha1Fingerprint, - docTypes = docTypes, - supportsDelete = HolderApp.certificateStorageEngineInstance.get( - this.certificate.javaX509Certificate.getSubjectKeyIdentifier() - ) != null, - trustPoint = this - ) -} - -private fun hexWithSpaces(byteArray: ByteArray): String { - val stringBuilder = StringBuilder() - byteArray.forEach { - if (stringBuilder.isNotEmpty()) { - stringBuilder.append(" ") - } - stringBuilder.append(String.format("%02X", it)) - } - return stringBuilder.toString() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt deleted file mode 100644 index 1919fe1a4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.android.identity.wallet.settings - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.theme.HolderAppTheme - -class SettingsFragment : Fragment() { - - private val settingsViewModel: SettingsViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - val state = settingsViewModel.settingsState.collectAsState().value - SettingsScreen( - modifier = Modifier.fillMaxSize(), - screenState = state, - onAutoCloseChanged = settingsViewModel::onConnectionAutoCloseChanged, - onSessionEncryptionCurveChanged = settingsViewModel::onEphemeralKeyCurveChanged, - onUseStaticHandoverChanged = settingsViewModel::onUseStaticHandoverChanged, - onUseL2CAPChanged = settingsViewModel::onL2CAPChanged, - onBLEDataRetrievalModeChanged = settingsViewModel::onBleDataRetrievalChanged, - onBLEServiceCacheChanged = settingsViewModel::onBleServiceCacheChanged, - onBLEPeripheralDataRetrievalModeChanged = settingsViewModel::onBlePeripheralModeChanged, - onWiFiAwareChanged = settingsViewModel::onWiFiAwareChanged, - onNfcChanged = settingsViewModel::onNFCChanged, - onDebugChanged = settingsViewModel::onDebugLoggingChanged, - onOpenCaCertificates = {openCaCertificates()}, - ) - } - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - settingsViewModel.loadSettings() - } - - private fun openCaCertificates(){ - val destination = SettingsFragmentDirections.toCaCertificates() - findNavController().navigate(destination) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt deleted file mode 100644 index 5de767056..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt +++ /dev/null @@ -1,296 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -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.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.composables.curveLabelFor -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun SettingsScreen( - modifier: Modifier = Modifier, - screenState: SettingsScreenState, - onAutoCloseChanged: (Boolean) -> Unit, - onSessionEncryptionCurveChanged: (newValue: SettingsScreenState.SessionEncryptionCurveOption) -> Unit, - onUseStaticHandoverChanged: (Boolean) -> Unit, - onUseL2CAPChanged: (Boolean) -> Unit, - onBLEServiceCacheChanged: (Boolean) -> Unit, - onBLEDataRetrievalModeChanged: (Boolean) -> Unit, - onBLEPeripheralDataRetrievalModeChanged: (Boolean) -> Unit, - onWiFiAwareChanged: (Boolean) -> Unit, - onNfcChanged: (Boolean) -> Unit, - onDebugChanged: (Boolean) -> Unit, - onOpenCaCertificates: () -> Unit, -) { - Column(modifier = modifier) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .padding(16.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - SettingSectionTitle(title = "General") - SettingToggle( - title = "Auto close connection", - subtitleOn = "Close connection after first response", - subtitleOff = "Don't close connection after first response", - isChecked = screenState.autoCloseEnabled, - onCheckedChange = onAutoCloseChanged - ) - SettingsDropDown( - title = "Session Encryption Curve", - description = curveLabelFor(screenState.sessionEncryptionCurveOption.toEcCurve()), - onCurveChanged = onSessionEncryptionCurveChanged - ) - SettingSectionTitle(title = "NFC Engagement") - SettingToggle( - title = "Use static handover", - subtitleOn = "Use static handover", - subtitleOff = "Use negotiated handover", - isChecked = screenState.useStaticHandover, - onCheckedChange = onUseStaticHandoverChanged - ) - SettingSectionTitle(title = "Data retrieval options") - SettingToggle( - title = "Use L2CAP if available", - subtitleOn = "Use L2CAP", - subtitleOff = "Don't use L2CAP", - isChecked = screenState.isL2CAPEnabled, - enabled = screenState.isBleEnabled(), - onCheckedChange = onUseL2CAPChanged - ) - SettingToggle( - title = "Clear BLE Service Cache", - subtitleOn = "Clean the cache", - subtitleOff = "Don't clean the cache", - isChecked = screenState.isBleClearCacheEnabled, - enabled = screenState.isBleEnabled(), - onCheckedChange = onBLEServiceCacheChanged - ) - SettingSectionTitle(title = "Data retrieval methods") - SettingToggle( - title = "BLE central client mode", - subtitleOn = "BLE central client mode activated", - subtitleOff = "BLE central client mode deactivated", - isChecked = screenState.isBleDataRetrievalEnabled, - onCheckedChange = onBLEDataRetrievalModeChanged - ) - SettingToggle( - title = "BLE peripheral server mode", - subtitleOn = "BLE peripheral server mode activated", - subtitleOff = "BLE peripheral server mode deactivated", - isChecked = screenState.isBlePeripheralModeEnabled, - onCheckedChange = onBLEPeripheralDataRetrievalModeChanged - ) - SettingToggle( - title = "Wifi Aware", - subtitleOn = "Wifi Aware transfer activated", - subtitleOff = "Wifi Aware transfer deactivated", - isChecked = screenState.wifiAwareEnabled, - onCheckedChange = onWiFiAwareChanged - ) - SettingToggle( - title = "NFC", - subtitleOn = "NFC transfer activated", - subtitleOff = "NFC transfer deactivated", - isChecked = screenState.nfcEnabled, - onCheckedChange = onNfcChanged - ) - SettingSectionTitle(title = "Debug logging options") - SettingToggle( - title = "Debug", - subtitleOn = "Debug logging activated", - subtitleOff = "Debug logging deactivated", - isChecked = screenState.debugEnabled, - onCheckedChange = onDebugChanged - ) - SettingSectionTitle( - title = "CA Certificates" - ) - SettingItem( - modifier = Modifier - .clickable { onOpenCaCertificates() }, - title = "Show CA Certificates", - subtitle = "Click here to show the CA Certificates" - ) - } - } -} - -@Composable -private fun SettingSectionTitle( - modifier: Modifier = Modifier, - title: String -) { - Column(modifier = modifier) { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) - } -} - -@Composable -private fun SettingItem( - modifier: Modifier = Modifier, - title: String, - subtitle: String -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - } -} - -@Composable -private fun SettingToggle( - modifier: Modifier = Modifier, - title: String, - subtitleOn: String, - subtitleOff: String, - isChecked: Boolean, - onCheckedChange: (Boolean) -> Unit, - enabled: Boolean = true -) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - val subtitle = if (isChecked) subtitleOn else subtitleOff - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - Switch( - checked = isChecked, - enabled = enabled, - onCheckedChange = onCheckedChange - ) - } -} - -@Composable -private fun SettingsDropDown( - modifier: Modifier = Modifier, - title: String, - description: String, - onCurveChanged: (selection: SettingsScreenState.SessionEncryptionCurveOption) -> Unit -) { - var dropDownExpanded by remember { mutableStateOf(false) } - val expandDropDown = { dropDownExpanded = true } - Row( - modifier = modifier.clickable { expandDropDown() }, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - IconButton(onClick = expandDropDown) { - Icon( - imageVector = Icons.Default.ArrowDropDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - val entries = SettingsScreenState.SessionEncryptionCurveOption.values().toList() - DropdownMenu( - expanded = dropDownExpanded, - onDismissRequest = { dropDownExpanded = false } - ) { - for (entry in entries) { - DropdownMenuItem( - modifier = modifier, - text = { - Text( - modifier = Modifier.fillMaxWidth(), - text = curveLabelFor(curveOption = entry.toEcCurve()) - ) - }, - onClick = { - onCurveChanged(entry) - dropDownExpanded = false - } - ) - } - } - } -} - - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SettingsScreenPreview() { - HolderAppTheme { - SettingsScreen( - modifier = Modifier.fillMaxSize(), - screenState = SettingsScreenState(), - onAutoCloseChanged = {}, - onSessionEncryptionCurveChanged = {}, - onUseStaticHandoverChanged = {}, - onUseL2CAPChanged = {}, - onBLEServiceCacheChanged = {}, - onBLEDataRetrievalModeChanged = {}, - onBLEPeripheralDataRetrievalModeChanged = {}, - onWiFiAwareChanged = {}, - onNfcChanged = {}, - onDebugChanged = {}, - onOpenCaCertificates = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt deleted file mode 100644 index dacbd9a9e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.android.identity.wallet.settings - -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import com.android.identity.crypto.EcCurve -import kotlinx.parcelize.Parcelize - -@Stable -@Immutable -data class SettingsScreenState( - val autoCloseEnabled: Boolean = true, - val sessionEncryptionCurveOption: SessionEncryptionCurveOption = SessionEncryptionCurveOption.P256, - val useStaticHandover: Boolean = true, - val isL2CAPEnabled: Boolean = false, - val isBleClearCacheEnabled: Boolean = false, - val isBleDataRetrievalEnabled: Boolean = true, - val isBlePeripheralModeEnabled: Boolean = false, - val wifiAwareEnabled: Boolean = false, - val nfcEnabled: Boolean = false, - val debugEnabled: Boolean = true -) { - - fun isBleEnabled(): Boolean = isBleDataRetrievalEnabled || isBlePeripheralModeEnabled - - fun canToggleBleDataRetrievalMode(newBleCentralMode: Boolean): Boolean { - val updatedState = copy(isBleDataRetrievalEnabled = newBleCentralMode) - return updatedState.hasDataRetrieval() - } - - fun canToggleBlePeripheralMode(newBlePeripheralMode: Boolean): Boolean { - val updatedState = copy(isBlePeripheralModeEnabled = newBlePeripheralMode) - return updatedState.hasDataRetrieval() - } - - fun canToggleWifiAware(newWifiAwareValue: Boolean): Boolean { - val updatedState = copy(wifiAwareEnabled = newWifiAwareValue) - return updatedState.hasDataRetrieval() - } - - fun canToggleNfc(newNfcValue: Boolean): Boolean { - val updatedState = copy(nfcEnabled = newNfcValue) - return updatedState.hasDataRetrieval() - } - - private fun hasDataRetrieval(): Boolean = - isBleDataRetrievalEnabled - || isBlePeripheralModeEnabled - || wifiAwareEnabled - || nfcEnabled - - @Parcelize - enum class SessionEncryptionCurveOption : Parcelable { - P256, - P384, - P521, - BrainPoolP256R1, - BrainPoolP320R1, - BrainPoolP384R1, - BrainPoolP512R1, - X25519, - X448; - - fun toEcCurve(): EcCurve = - when (this) { - P256 -> EcCurve.P256 - P384 -> EcCurve.P384 - P521 -> EcCurve.P521 - BrainPoolP256R1 -> EcCurve.BRAINPOOLP256R1 - BrainPoolP320R1 -> EcCurve.BRAINPOOLP320R1 - BrainPoolP384R1 -> EcCurve.BRAINPOOLP384R1 - BrainPoolP512R1 -> EcCurve.BRAINPOOLP512R1 - X25519 -> EcCurve.X25519 - X448 -> EcCurve.X448 - } - - companion object { - fun fromEcCurve(curve: EcCurve): SessionEncryptionCurveOption = - when (curve) { - EcCurve.P256 -> P256 - EcCurve.P384 -> P384 - EcCurve.P521 -> P521 - EcCurve.BRAINPOOLP256R1 -> BrainPoolP256R1 - EcCurve.BRAINPOOLP320R1 -> BrainPoolP320R1 - EcCurve.BRAINPOOLP384R1 -> BrainPoolP384R1 - EcCurve.BRAINPOOLP512R1 -> BrainPoolP512R1 - EcCurve.X25519 -> X25519 - EcCurve.X448 -> X448 - else -> throw IllegalStateException("Unknown EcCurve") - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt deleted file mode 100644 index 0526dd243..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.android.identity.wallet.settings - -import androidx.lifecycle.ViewModel -import com.android.identity.wallet.util.PreferencesHelper -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -class SettingsViewModel : ViewModel() { - - private val mutableSettingsState = MutableStateFlow(SettingsScreenState()) - val settingsState: StateFlow = mutableSettingsState - - fun loadSettings() { - val settingsState = SettingsScreenState( - autoCloseEnabled = PreferencesHelper.isConnectionAutoCloseEnabled(), - sessionEncryptionCurveOption = SettingsScreenState.SessionEncryptionCurveOption.fromEcCurve( - PreferencesHelper.getEphemeralKeyCurveOption() - ), - useStaticHandover = PreferencesHelper.shouldUseStaticHandover(), - isL2CAPEnabled = PreferencesHelper.isBleL2capEnabled(), - isBleClearCacheEnabled = PreferencesHelper.isBleClearCacheEnabled(), - isBleDataRetrievalEnabled = PreferencesHelper.isBleDataRetrievalEnabled(), - isBlePeripheralModeEnabled = PreferencesHelper.isBleDataRetrievalPeripheralModeEnabled(), - wifiAwareEnabled = PreferencesHelper.isWifiDataRetrievalEnabled(), - nfcEnabled = PreferencesHelper.isNfcDataRetrievalEnabled(), - debugEnabled = PreferencesHelper.isDebugLoggingEnabled() - ) - mutableSettingsState.value = settingsState - } - - fun onConnectionAutoCloseChanged(newValue: Boolean) { - PreferencesHelper.setConnectionAutoCloseEnabled(newValue) - mutableSettingsState.update { it.copy(autoCloseEnabled = newValue) } - } - - fun onEphemeralKeyCurveChanged( - sessionEncryptionCurveOption: SettingsScreenState.SessionEncryptionCurveOption - ) { - PreferencesHelper.setEphemeralKeyCurveOption(sessionEncryptionCurveOption.toEcCurve()) - mutableSettingsState.update { it.copy(sessionEncryptionCurveOption = sessionEncryptionCurveOption) } - } - - fun onUseStaticHandoverChanged(newValue: Boolean) { - PreferencesHelper.setUseStaticHandover(newValue) - mutableSettingsState.update { it.copy(useStaticHandover = newValue) } - } - - fun onL2CAPChanged(newValue: Boolean) { - PreferencesHelper.setBleL2CAPEnabled(newValue) - mutableSettingsState.update { it.copy(isL2CAPEnabled = newValue) } - } - - fun onBleServiceCacheChanged(newValue: Boolean) { - PreferencesHelper.setBleClearCacheEnabled(newValue) - mutableSettingsState.update { it.copy(isBleClearCacheEnabled = newValue) } - } - - fun onBleDataRetrievalChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleBleDataRetrievalMode(newValue)) { - PreferencesHelper.setBleDataRetrievalEnabled(newValue) - mutableSettingsState.update { it.copy(isBleDataRetrievalEnabled = newValue) } - } - } - - fun onBlePeripheralModeChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleBlePeripheralMode(newValue)) { - PreferencesHelper.setBlePeripheralDataRetrievalMode(newValue) - mutableSettingsState.update { it.copy(isBlePeripheralModeEnabled = newValue) } - } - } - - fun onWiFiAwareChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleWifiAware(newValue)) { - PreferencesHelper.setWifiDataRetrievalEnabled(newValue) - mutableSettingsState.update { it.copy(wifiAwareEnabled = newValue) } - } - } - - fun onNFCChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleNfc(newValue)) { - PreferencesHelper.setNfcDataRetrievalEnabled(newValue) - mutableSettingsState.update { it.copy(nfcEnabled = newValue) } - } - } - - fun onDebugLoggingChanged(newValue: Boolean) { - PreferencesHelper.setDebugLoggingEnabled(newValue) - mutableSettingsState.update { it.copy(debugEnabled = newValue) } - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt deleted file mode 100644 index f9d518c7b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt +++ /dev/null @@ -1,227 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Handler -import android.os.Looper -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import com.android.identity.android.securearea.AndroidKeystoreCreateKeySettings -import com.android.identity.android.securearea.AndroidKeystoreKeyInfo -import com.android.identity.android.securearea.AndroidKeystoreKeyUnlockData -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.securearea.UserAuthenticationType -import com.android.identity.android.securearea.userAuthenticationTypeSet -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborMap -import com.android.identity.crypto.Algorithm -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.crypto.EcCurve -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.wallet.R -import com.android.identity.wallet.authprompt.UserAuthPromptBuilder -import com.android.identity.wallet.composables.AndroidSetupContainer -import com.android.identity.wallet.composables.AuthenticationKeyCurveAndroid -import com.android.identity.wallet.composables.MdocAuthentication -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOption -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState -import com.android.identity.wallet.composables.state.AuthTypeState -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.datetime.Instant - -class AndroidKeystoreSecureAreaSupport( - private val capabilities: AndroidKeystoreSecureArea.Capabilities -) : SecureAreaSupport { - - private val screenState = AndroidKeystoreSecureAreaSupportState( - allowLSKFUnlocking = AuthTypeState(true, capabilities.multipleAuthenticationTypesSupported), - allowBiometricUnlocking = AuthTypeState(true, capabilities.multipleAuthenticationTypesSupported), - useStrongBox = AuthTypeState(false, capabilities.strongBoxSupported), - mDocAuthOption = MdocAuthOption(isEnabled = capabilities.keyAgreementSupported), - authKeyCurveState = AndroidAuthKeyCurveState(isEnabled = capabilities.curve25519Supported) - ) - - override fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) { - val keyInfo = credential.secureArea.getKeyInfo(credential.alias) as AndroidKeystoreKeyInfo - val unlockData = AndroidKeystoreKeyUnlockData(credential.alias) - - val allowLskf = keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.LSKF) - val allowBiometric = keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.BIOMETRIC) - val allowBoth = keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.LSKF) && - keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.BIOMETRIC) - val allowLskfUnlock = allowLskf || allowBoth - val allowBiometricUnlock = allowBiometric || allowBoth - val forceLskf: Boolean = !allowBiometricUnlock - - val userAuthRequest = UserAuthPromptBuilder.requestUserAuth(this) - .withTitle(getString(R.string.bio_auth_title)) - .withSuccessCallback { - onKeyUnlocked(unlockData) - } - .withCancelledCallback { - if (allowLskfUnlock) { - val runnable = { - unlockKey(credential, onKeyUnlocked, onUnlockFailure) - } - // Without this delay, the prompt won't reshow - Handler(Looper.getMainLooper()).postDelayed(runnable, 100) - } else { - onUnlockFailure(true) - } - } - .withFailureCallback { onUnlockFailure(false) } - .setForceLskf(forceLskf) - if (allowLskfUnlock) { - userAuthRequest.withNegativeButton(getString(R.string.bio_auth_use_pin)) - } else { - userAuthRequest.withNegativeButton("Cancel") - } - val cryptoObject = unlockData.getCryptoObjectForSigning(Algorithm.ES256) - userAuthRequest.build().authenticate(cryptoObject) - } - - @Composable - override fun SecureAreaAuthUi( - onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit - ) { - var compositionState by remember { mutableStateOf(screenState) } - LaunchedEffect(key1 = compositionState) { - onUiStateUpdated(compositionState) - } - AndroidSetupContainer( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - isOn = compositionState.userAuthentication, - timeoutSeconds = compositionState.userAuthenticationTimeoutSeconds, - lskfAuthTypeState = compositionState.allowLSKFUnlocking, - biometricAuthTypeState = compositionState.allowBiometricUnlocking, - useStrongBox = compositionState.useStrongBox, - onUserAuthenticationChanged = { - compositionState = compositionState.copy(userAuthentication = it) - }, - onAuthTimeoutChanged = { seconds -> - if (seconds < 0) return@AndroidSetupContainer - compositionState = compositionState.copy(userAuthenticationTimeoutSeconds = seconds) - }, - onLskfAuthChanged = { - val allowLskfUnlock = - if (compositionState.allowBiometricUnlocking.isEnabled) it else true - val newValue = compositionState.allowLSKFUnlocking.copy(isEnabled = allowLskfUnlock) - compositionState = compositionState.copy(allowLSKFUnlocking = newValue) - }, - onBiometricAuthChanged = { - val allowBiometricUnlock = - if (compositionState.allowLSKFUnlocking.isEnabled) it else true - val newValue = - compositionState.allowBiometricUnlocking.copy(isEnabled = allowBiometricUnlock) - compositionState = compositionState.copy(allowBiometricUnlocking = newValue) - }, - onStrongBoxChanged = { newValue -> - val update = compositionState.copy( - useStrongBox = compositionState.useStrongBox.copy(isEnabled = newValue), - mDocAuthOption = MdocAuthOption( - isEnabled = if (newValue) capabilities.strongBoxKeyAgreementSupported else capabilities.keyAgreementSupported - ), - authKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (newValue) capabilities.strongBoxCurve25519Supported else capabilities.curve25519Supported - ) - ) - compositionState = update - } - ) - MdocAuthentication( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.mDocAuthOption, - onMdocAuthOptionChange = { newValue -> - val authState = compositionState.mDocAuthOption.copy(mDocAuthentication = newValue) - compositionState = compositionState.copy( - mDocAuthOption = authState, - authKeyCurveState = compositionState.authKeyCurveState.copy( - authCurve = AndroidAuthKeyCurveOption.P_256 - ) - ) - } - ) - AuthenticationKeyCurveAndroid( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.authKeyCurveState, - mDocAuthState = compositionState.mDocAuthOption, - onAndroidAuthKeyCurveChanged = { - val newValue = compositionState.authKeyCurveState.copy(authCurve = it) - compositionState = compositionState.copy(authKeyCurveState = newValue) - } - ) - } - - override fun getSecureAreaSupportState(): SecureAreaSupportState { - return screenState - } - - override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray { - val state = secureAreaSupportState as AndroidKeystoreSecureAreaSupportState - - val userAuthSettings = mutableSetOf() - if (state.allowLSKFUnlocking.isEnabled) { - userAuthSettings.add(UserAuthenticationType.LSKF) - } - if (state.allowBiometricUnlocking.isEnabled) { - userAuthSettings.add(UserAuthenticationType.BIOMETRIC) - } - - return Cbor.encode( - CborMap.builder() - .put("curve", state.authKeyCurveState.authCurve.toEcCurve().coseCurveIdentifier.toLong()) - .put("purposes", KeyPurpose.encodeSet(setOf(state.mDocAuthOption.mDocAuthentication.toKeyPurpose()))) - .put("userAuthEnabled", state.userAuthentication) - .put("userAuthTimeoutMillis", state.userAuthenticationTimeoutSeconds.toLong() * 1000L) - .put("userAuthSettings", UserAuthenticationType.encodeSet(userAuthSettings)) - .put("useStrongBox", state.useStrongBox.isEnabled) - .end() - .build() - ) - } - - override fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings { - val map = Cbor.decode(encodedConfiguration) - val curve = EcCurve.fromInt(map["curve"].asNumber.toInt()) - val purposes = KeyPurpose.decodeSet(map["purposes"].asNumber) - val userAuthEnabled = map["userAuthEnabled"].asBoolean - val userAuthTimeoutMillis = map["userAuthTimeoutMillis"].asNumber - val userAuthSettings = map["userAuthSettings"].asNumber - val useStrongBox = map["useStrongBox"].asBoolean - return AndroidKeystoreCreateKeySettings.Builder(challenge) - .setEcCurve(curve) - .setKeyPurposes(purposes) - .setValidityPeriod(validFrom, validUntil) - .setUseStrongBox(useStrongBox) - .setUserAuthenticationRequired( - userAuthEnabled, - userAuthTimeoutMillis, - userAuthSettings.userAuthenticationTypeSet - ) - .build() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt deleted file mode 100644 index 8e75167b2..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState -import com.android.identity.wallet.composables.state.AuthTypeState -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AndroidKeystoreSecureAreaSupportState( - override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), - val userAuthentication: Boolean = true, - val userAuthenticationTimeoutSeconds: Int = 0, - val allowLSKFUnlocking: AuthTypeState = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - val allowBiometricUnlocking: AuthTypeState = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - val useStrongBox: AuthTypeState = AuthTypeState( - isEnabled = false, - canBeModified = false - ), - val authKeyCurveState: AndroidAuthKeyCurveState = AndroidAuthKeyCurveState(), -) : SecureAreaSupportState { - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt b/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt deleted file mode 100644 index 7508e8571..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Parcelable -import com.android.identity.securearea.SecureArea -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class CurrentSecureArea( - @IgnoredOnParcel val secureArea: SecureArea, - val identifier: String = secureArea.identifier, - val displayName: String = secureArea.displayName -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt deleted file mode 100644 index 14088a783..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.securearea.KeyPurpose -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -fun MdocAuthStateOption.toKeyPurpose(): KeyPurpose = - if (this == MdocAuthStateOption.ECDSA) { - KeyPurpose.SIGN - } else { - KeyPurpose.AGREE_KEY - } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt deleted file mode 100644 index 35c697d76..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.android.identity.wallet.support - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.fragment.app.Fragment -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.document.Document -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.securearea.SecureArea -import com.android.identity.securearea.software.SoftwareSecureArea -import kotlinx.datetime.Instant - -interface SecureAreaSupport { - - /** - * This function should create a composable that will render the portion of the UI - * for the specific [SecureArea] setup inside the [AddSelfSignedDocumentScreen]. - * - * The composable should hold and manage its state internally, and expose it through - * the [onUiStateUpdated] lambda. The state must be an implementation of [SecureAreaSupportState] - */ - @Composable - fun SecureAreaAuthUi(onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit) - - /** - * This function should create [SecureArea.KeyUnlockData] based on the incoming - * [Document.AuthenticationKey]. Its implementation should decide on the mechanism - * that will do the unlocking (i.e. present a biometric prompts or other sort of UI), - * and the way the [SecureArea.KeyUnlockData] is created. - * - * The function is an extension on a [Fragment] due to the nature of Android and the navigation, - * so in case of rendering a UI specific for unlocking (like a Biometric Prompt, or a Dialog), - * there is a provided way to navigate using the [findNavController] function. - */ - fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) - - /** - * Should return the current [SecureAreaSupportState] which is used by the composition - * when rendering the composable UI. - */ - fun getSecureAreaSupportState(): SecureAreaSupportState - - /** - * Returns a configuration for creating authentication keys which can be persisted to disk - * and passed to [createAuthKeySettingsFromConfiguration] every time new authentication keys - * need to be created. - */ - fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray - - /** - * Creates a [SecureArea.CreateKeySettings] instead based on the settings previously created - * with [createAuthKeySettingsConfiguration] and the given challenge and validity period. - */ - fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings - - companion object { - fun getInstance( - context: Context, - currentSecureArea: CurrentSecureArea - ): SecureAreaSupport { - return when (currentSecureArea.secureArea) { - is AndroidKeystoreSecureArea -> { - val capabilities = AndroidKeystoreSecureArea.Capabilities(context) - AndroidKeystoreSecureAreaSupport(capabilities) - } - - is SoftwareSecureArea -> SoftwareKeystoreSecureAreaSupport() - - else -> SecureAreaSupportNull() - } - } - - fun getInstance( - context: Context, - secureArea: SecureArea, - ): SecureAreaSupport { - return when (secureArea) { - is AndroidKeystoreSecureArea -> { - val capabilities = AndroidKeystoreSecureArea.Capabilities(context) - AndroidKeystoreSecureAreaSupport(capabilities) - } - - is SoftwareSecureArea -> SoftwareKeystoreSecureAreaSupport() - - else -> SecureAreaSupportNull() - } - } - } -} - -/** - * Utility function to convert a [SecureArea] implementation into a corresponding state - * used by the Jetpack Compose composition when rendering the UI. - */ -fun SecureArea.toSecureAreaState(): CurrentSecureArea { - return CurrentSecureArea(this) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt deleted file mode 100644 index 9467ac0f6..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.android.identity.wallet.support - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.wallet.selfsigned.OutlinedContainerVertical -import kotlinx.datetime.Instant - -class SecureAreaSupportNull : SecureAreaSupport { - - private val state = SecureAreaSupportStateNull() - - @Composable - override fun SecureAreaAuthUi(onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit) { - val compositionState by remember { mutableStateOf(state) } - LaunchedEffect(key1 = compositionState) { - onUiStateUpdated(compositionState) - } - OutlinedContainerVertical( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - Text( - modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp), - text = "The selected Secure Area lacks dedicated support so no additional options are available. " + - "Please add a SecureAreaSupport-derived class.", - style = MaterialTheme.typography.bodySmall - ) - } - } - - override fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) { - throw IllegalStateException("No implementation") - } - - override fun getSecureAreaSupportState(): SecureAreaSupportState = state - - override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray = - ByteArray(0) - - override fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings = CreateKeySettings() -} diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt deleted file mode 100644 index 864f32447..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Parcelable -import com.android.identity.wallet.composables.state.MdocAuthOption - -interface SecureAreaSupportState : Parcelable { - val mDocAuthOption: MdocAuthOption -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt deleted file mode 100644 index 74a419f7e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.parcelize.Parcelize - -@Parcelize -class SecureAreaSupportStateNull : SecureAreaSupportState { - - override val mDocAuthOption: MdocAuthOption - get() = MdocAuthOption() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt deleted file mode 100644 index 040ca0d28..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Handler -import android.os.Looper -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import co.nstant.`in`.cbor.CborBuilder -import com.android.identity.cbor.Cbor -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.crypto.EcCurve -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.securearea.software.SoftwareCreateKeySettings -import com.android.identity.securearea.software.SoftwareKeyUnlockData -import com.android.identity.wallet.authconfirmation.AuthConfirmationFragmentDirections -import com.android.identity.wallet.authconfirmation.PassphraseAuthResult -import com.android.identity.wallet.authconfirmation.PassphrasePromptViewModel -import com.android.identity.wallet.composables.AuthenticationKeyCurveSoftware -import com.android.identity.wallet.composables.MdocAuthentication -import com.android.identity.wallet.composables.SoftwareSetupContainer -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveOption -import com.android.identity.wallet.util.FormatUtil -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant - -class SoftwareKeystoreSecureAreaSupport : SecureAreaSupport { - - private val screenState = SoftwareKeystoreSecureAreaSupportState() - - override fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) { - val viewModel: PassphrasePromptViewModel by activityViewModels() - var didAttemptToUnlock = false - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.authorizationState.collect { value -> - if (value is PassphraseAuthResult.Success) { - val keyUnlockData = SoftwareKeyUnlockData(value.userPassphrase) - didAttemptToUnlock = true - onKeyUnlocked(keyUnlockData) - viewModel.reset() - } - } - } - } - val destination = AuthConfirmationFragmentDirections.openPassphrasePrompt( - showIncorrectPassword = didAttemptToUnlock - ) - val runnable = { findNavController().navigate(destination) } - // The system needs a little time to get back to this screen - Handler(Looper.getMainLooper()).postDelayed(runnable, 500) - } - - @Composable - override fun SecureAreaAuthUi( - onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit - ) { - var compositionState by remember { mutableStateOf(screenState) } - LaunchedEffect(key1 = compositionState) { - onUiStateUpdated(compositionState) - } - SoftwareSetupContainer( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - passphrase = compositionState.passphrase, - onPassphraseChanged = { - compositionState = compositionState.copy(passphrase = it) - } - ) - MdocAuthentication( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.mDocAuthOption, - onMdocAuthOptionChange = { - val newValue = compositionState.mDocAuthOption.copy(mDocAuthentication = it) - compositionState = compositionState.copy( - mDocAuthOption = newValue, - softwareAuthKeyCurveState = compositionState.softwareAuthKeyCurveState.copy( - authCurve = SoftwareAuthKeyCurveOption.P256 - ) - ) - } - ) - AuthenticationKeyCurveSoftware( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.softwareAuthKeyCurveState, - mDocAuthState = compositionState.mDocAuthOption, - onSoftwareAuthKeyCurveChanged = { - val newValue = compositionState.authKeyCurve.copy(authCurve = it) - compositionState = compositionState.copy(softwareAuthKeyCurveState = newValue) - } - ) - } - - override fun getSecureAreaSupportState(): SecureAreaSupportState { - return screenState - } - - override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray { - val state = secureAreaSupportState as SoftwareKeystoreSecureAreaSupportState - return FormatUtil.cborEncode( - CborBuilder() - .addMap() - .put("curve", state.softwareAuthKeyCurveState.authCurve.toEcCurve().coseCurveIdentifier.toLong()) - .put("purposes", KeyPurpose.encodeSet( - setOf(state.mDocAuthOption.mDocAuthentication.toKeyPurpose())).toLong()) - .put("passphraseRequired", state.passphrase.isNotEmpty()) - .put("passphrase", state.passphrase) - .end() - .build().get(0)) - } - - override fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings { - val map = Cbor.decode(encodedConfiguration) - val curve = EcCurve.fromInt(map["curve"].asNumber.toInt()) - val purposes = KeyPurpose.decodeSet(map["purposes"].asNumber) - val passphraseRequired = map["passphraseRequired"].asBoolean - val passphrase = map["passphrase"].asTstr - return SoftwareCreateKeySettings.Builder() - .setEcCurve(curve) - .setKeyPurposes(purposes) - .setValidityPeriod(validFrom, validUntil) - .setPassphraseRequired(passphraseRequired, passphrase, null) - .build() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt deleted file mode 100644 index 43b8487a4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveState -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.parcelize.Parcelize - -@Parcelize -data class SoftwareKeystoreSecureAreaSupportState( - override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), - val softwareAuthKeyCurveState: SoftwareAuthKeyCurveState = SoftwareAuthKeyCurveState(), - val passphrase: String = "", - val authKeyCurve: SoftwareAuthKeyCurveState = SoftwareAuthKeyCurveState(), -) : SecureAreaSupportState { - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt deleted file mode 100644 index 7f56aefa5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.support.androidkeystore - -import android.os.Parcelable -import com.android.identity.crypto.EcCurve -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class AndroidAuthKeyCurveOption : Parcelable { - P_256, Ed25519, X25519; - - fun toEcCurve(): EcCurve = - when (this) { - P_256 -> EcCurve.P256 - Ed25519 -> EcCurve.ED25519 - X25519 -> EcCurve.X25519 - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt deleted file mode 100644 index 80d8cbde8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.support.androidkeystore - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AndroidAuthKeyCurveState( - val isEnabled: Boolean = true, - val authCurve: AndroidAuthKeyCurveOption = AndroidAuthKeyCurveOption.P_256 -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt deleted file mode 100644 index aea270074..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.identity.wallet.support.softwarekeystore - -import android.os.Parcelable -import com.android.identity.crypto.EcCurve -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class SoftwareAuthKeyCurveOption : Parcelable { - P256, - P384, - P521, - BrainPoolP256R1, - BrainPoolP320R1, - BrainPoolP384R1, - BrainPoolP512R1, - Ed25519, - Ed448, - X25519, - X448; - - fun toEcCurve(): EcCurve = - when (this) { - P256 -> EcCurve.P256 - P384 -> EcCurve.P384 - P521 -> EcCurve.P521 - BrainPoolP256R1 -> EcCurve.BRAINPOOLP256R1 - BrainPoolP320R1 -> EcCurve.BRAINPOOLP320R1 - BrainPoolP384R1 -> EcCurve.BRAINPOOLP384R1 - BrainPoolP512R1 -> EcCurve.BRAINPOOLP512R1 - Ed25519 -> EcCurve.ED25519 - Ed448 -> EcCurve.ED448 - X25519 -> EcCurve.X25519 - X448 -> EcCurve.X448 - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt deleted file mode 100644 index f7004ee44..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.support.softwarekeystore - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class SoftwareAuthKeyCurveState( - val isEnabled: Boolean = true, - val authCurve: SoftwareAuthKeyCurveOption = SoftwareAuthKeyCurveOption.P256 -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Color.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Color.kt deleted file mode 100644 index 3cd8bccb9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Color.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.identity.wallet.theme - -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color - -//Light Theme -val ThemeLightPrimary = Color(0xFF476810) -val ThemeLightOnPrimary = Color(0xFFFFFFFF) -val ThemeLightPrimaryContainer = Color(0xFFC7F089) - -//Dark Theme -val ThemeDarkPrimary = Color(0xFFACD370) -val ThemeDarkOnPrimary = Color(0xFF213600) -val ThemeDarkPrimaryContainer = Color(0xFF324F00) - -val GreenLight = Color(0xFF00E676) -val GreenDark = Color(0xFF34A853) -val GreenGradient = Brush.linearGradient(colors = listOf(GreenDark, GreenLight)) - -val BlueLight = Color(0xFF00B0FF) -val BlueDark = Color(0xFF4285F4) -val BlueGradient = Brush.linearGradient(colors = listOf(BlueDark, BlueLight)) - -val YellowLight = Color(0xFFFFEA00) -val YellowDark = Color(0xFFFBBC05) -val YellowGradient = Brush.linearGradient(colors = listOf(YellowDark, YellowLight)) - -val RedLight = Color(0xFFFF5722) -val RedDark = Color(0xFFF44336) -val RedGradient = Brush.linearGradient(colors = listOf(RedDark, RedLight)) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt deleted file mode 100644 index 6edaebc19..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.theme - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Shapes -import androidx.compose.ui.unit.dp - -val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(4.dp), - large = RoundedCornerShape(0.dp) -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt deleted file mode 100644 index d5c9b31d8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.android.identity.wallet.theme - -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.ui.platform.LocalContext - -private val lightColorPalette = lightColorScheme( - primary = ThemeLightPrimary, - onPrimary = ThemeLightOnPrimary, - primaryContainer = ThemeLightPrimaryContainer, - onPrimaryContainer = ThemeLightPrimaryContainer, -) - -private val darkColorPalette = darkColorScheme( - primary = ThemeDarkPrimary, - onPrimary = ThemeDarkOnPrimary, - primaryContainer = ThemeDarkPrimaryContainer, - onPrimaryContainer = ThemeDarkPrimaryContainer, -) - -@Composable -fun HolderAppTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit -) { - - val darkColorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - dynamicDarkColorScheme(LocalContext.current) - } else { - darkColorPalette - } - - val lightColorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - dynamicLightColorScheme(LocalContext.current) - } else { - lightColorPalette - } - - val colors = when { - darkTheme -> darkColorScheme - else -> lightColorScheme - } - - MaterialTheme( - colorScheme = colors, - typography = Typography, - shapes = Shapes, - content = content - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Type.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Type.kt deleted file mode 100644 index d6341d7b3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Type.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.identity.wallet.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - bodyLarge = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 36.sp - ), - bodyMedium = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - ), - bodySmall = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - ) -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt deleted file mode 100644 index f73ff598f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.identity.wallet.transfer - -import com.android.identity.mdoc.credential.MdocCredential - -sealed class AddDocumentToResponseResult { - - data class DocumentAdded( - val signingKeyUsageLimitPassed: Boolean - ) : AddDocumentToResponseResult() - - data class DocumentLocked( - val credential: MdocCredential - ) : AddDocumentToResponseResult() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt deleted file mode 100644 index 908e273e2..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.annotation.SuppressLint -import android.content.Context -import com.android.identity.util.Constants -import com.android.identity.mdoc.request.DeviceRequestParser -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.mainExecutor -import java.util.OptionalLong - -class Communication private constructor( - private val context: Context, -) { - - private var request: DeviceRequest? = null - var deviceRetrievalHelper: DeviceRetrievalHelper? = null - - fun setDeviceRequest(deviceRequest: ByteArray) { - this.request = DeviceRequest(deviceRequest) - } - - fun getDeviceRequest(): DeviceRequestParser.DeviceRequest = - request?.let { requestBytes -> - deviceRetrievalHelper?.let { presentation -> - DeviceRequestParser( - requestBytes.value, - presentation.sessionTranscript - ).run { parse() } - } ?: throw IllegalStateException("Presentation not set") - } ?: throw IllegalStateException("Request not received") - - - fun getSessionTranscript(): ByteArray? = deviceRetrievalHelper?.sessionTranscript - - fun sendResponse(deviceResponse: ByteArray, closeAfterSending: Boolean) { - if (closeAfterSending) { - deviceRetrievalHelper?.sendDeviceResponse( - deviceResponse, - Constants.SESSION_DATA_STATUS_SESSION_TERMINATION) - deviceRetrievalHelper?.disconnect() - } else { - deviceRetrievalHelper?.sendDeviceResponse(deviceResponse, null) - } - } - - fun stopPresentation( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - if (sendSessionTerminationMessage) { - if (useTransportSpecificSessionTermination) { - deviceRetrievalHelper?.sendTransportSpecificTermination() - } else { - deviceRetrievalHelper?.sendDeviceResponse( - null, - Constants.SESSION_DATA_STATUS_SESSION_TERMINATION - ) - } - } - disconnect() - } - - fun disconnect() = - try { - request = null - deviceRetrievalHelper?.disconnect() - } catch (e: RuntimeException) { - log("Error ignored closing presentation", e) - } - - companion object { - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: Communication? = null - - fun getInstance(context: Context): Communication { - return instance ?: synchronized(this) { - instance ?: Communication(context).also { instance = it } - } - } - } - - @JvmInline - value class DeviceRequest(val value: ByteArray) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt deleted file mode 100644 index d15b5da13..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.content.Context -import com.android.identity.mdoc.connectionmethod.ConnectionMethod -import com.android.identity.mdoc.connectionmethod.ConnectionMethodBle -import com.android.identity.mdoc.connectionmethod.ConnectionMethodNfc -import com.android.identity.mdoc.connectionmethod.ConnectionMethodWifiAware -import com.android.identity.android.mdoc.transport.DataTransportOptions -import com.android.identity.util.UUID -import com.android.identity.wallet.util.PreferencesHelper -import java.util.ArrayList -import java.util.OptionalLong - -class ConnectionSetup( - private val context: Context -) { - - fun getConnectionOptions(): DataTransportOptions { - val builder = DataTransportOptions.Builder() - .setBleUseL2CAP(PreferencesHelper.isBleL2capEnabled()) - .setBleClearCache(PreferencesHelper.isBleClearCacheEnabled()) - return builder.build() - } - - fun getConnectionMethods(): List { - val connectionMethods = ArrayList() - if (PreferencesHelper.isBleDataRetrievalEnabled()) { - connectionMethods.add( - ConnectionMethodBle( - false, - true, - null, - UUID.randomUUID() - ) - ) - } - if (PreferencesHelper.isBleDataRetrievalPeripheralModeEnabled()) { - connectionMethods.add( - ConnectionMethodBle( - true, - false, - UUID.randomUUID(), - null - ) - ) - } - if (PreferencesHelper.isWifiDataRetrievalEnabled()) { - connectionMethods.add( - ConnectionMethodWifiAware( - null, - null, - null, - null - ) - ) - } - if (PreferencesHelper.isNfcDataRetrievalEnabled()) { - // TODO: Add API to ConnectionMethodNfc to get sizes appropriate for the device - connectionMethods.add( - ConnectionMethodNfc( - 0xffff, - 0x10000 - ) - ) - } - return connectionMethods - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt deleted file mode 100644 index 82e1a6402..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.content.Context -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.android.mdoc.engagement.QrEngagementHelper -import com.android.identity.android.mdoc.transport.DataTransport -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.mainExecutor - -class QrCommunicationSetup( - private val context: Context, - private val onConnecting: () -> Unit, - private val onDeviceRetrievalHelperReady: (deviceRetrievalHelper: DeviceRetrievalHelper) -> Unit, - private val onNewDeviceRequest: (request: ByteArray) -> Unit, - private val onDisconnected: (transportSpecificTermination: Boolean) -> Unit, - private val onCommunicationError: (error: Throwable) -> Unit, -) { - - private val settings = PreferencesHelper.apply { initialize(context) } - private val connectionSetup = ConnectionSetup(context) - private val eDeviceKey = Crypto.createEcPrivateKey(settings.getEphemeralKeyCurveOption()) - - private var deviceRetrievalHelper: DeviceRetrievalHelper? = null - private lateinit var qrEngagement: QrEngagementHelper - - val deviceEngagementUriEncoded: String - get() = qrEngagement.deviceEngagementUriEncoded - - private val qrEngagementListener = object : QrEngagementHelper.Listener { - - override fun onDeviceConnecting() { - log("QR Engagement: Device Connecting") - onConnecting() - } - - override fun onDeviceConnected(transport: DataTransport) { - if (deviceRetrievalHelper != null) { - log("OnDeviceConnected for QR engagement -> ignoring due to active presentation") - return - } - log("OnDeviceConnected via QR: qrEngagement=$qrEngagement") - val builder = DeviceRetrievalHelper.Builder( - context, - deviceRetrievalHelperListener, - context.mainExecutor(), - eDeviceKey - ) - builder.useForwardEngagement( - transport, - qrEngagement.deviceEngagement, - qrEngagement.handover - ) - deviceRetrievalHelper = builder.build() - qrEngagement.close() - onDeviceRetrievalHelperReady(requireNotNull(deviceRetrievalHelper)) - } - - override fun onError(error: Throwable) { - log("QR onError: ${error.message}") - onCommunicationError(error) - } - } - - private val deviceRetrievalHelperListener = object : DeviceRetrievalHelper.Listener { - override fun onEReaderKeyReceived(eReaderKey: EcPublicKey) { - log("DeviceRetrievalHelper Listener (QR): OnEReaderKeyReceived") - } - - override fun onDeviceRequest(deviceRequestBytes: ByteArray) { - log("DeviceRetrievalHelper Listener (QR): OnDeviceRequest") - onNewDeviceRequest(deviceRequestBytes) - } - - override fun onDeviceDisconnected(transportSpecificTermination: Boolean) { - log("DeviceRetrievalHelper Listener (QR): onDeviceDisconnected") - onDisconnected(transportSpecificTermination) - } - - override fun onError(error: Throwable) { - log("DeviceRetrievalHelper Listener (QR): onError -> ${error.message}") - onCommunicationError(error) - } - } - - fun configure() { - qrEngagement = QrEngagementHelper.Builder( - context, - eDeviceKey.publicKey, - connectionSetup.getConnectionOptions(), - qrEngagementListener, - context.mainExecutor() - ).setConnectionMethods(connectionSetup.getConnectionMethods()) - .build() - } - - fun close() { - try { - qrEngagement.close() - } catch (exception: RuntimeException) { - log("Error closing QR engagement", exception) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt deleted file mode 100644 index df51050be..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.content.Context -import android.net.Uri -import android.util.Base64 -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.android.mdoc.transport.DataTransport -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey -import com.android.identity.mdoc.engagement.EngagementParser -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.mainExecutor - -class ReverseQrCommunicationSetup( - private val context: Context, - private val onPresentationReady: (presentation: DeviceRetrievalHelper) -> Unit, - private val onNewRequest: (request: ByteArray) -> Unit, - private val onDisconnected: () -> Unit, - private val onCommunicationError: (error: Throwable) -> Unit, -) { - - private val settings = PreferencesHelper.apply { initialize(context) } - private val connectionSetup = ConnectionSetup(context) - private val eDeviceKey = Crypto.createEcPrivateKey(settings.getEphemeralKeyCurveOption()) - - private var presentation: DeviceRetrievalHelper? = null - - private val presentationListener = object : DeviceRetrievalHelper.Listener { - override fun onEReaderKeyReceived(eReaderKey: EcPublicKey) { - log("DeviceRetrievalHelper Listener (QR): OnEReaderKeyReceived") - } - - override fun onDeviceRequest(deviceRequestBytes: ByteArray) { - onNewRequest(deviceRequestBytes) - } - - override fun onDeviceDisconnected(transportSpecificTermination: Boolean) { - onDisconnected() - } - - override fun onError(error: Throwable) { - onCommunicationError(error) - } - } - - fun configure( - reverseEngagementUri: String, - origins: List - ) { - val uri = Uri.parse(reverseEngagementUri) - if (!uri.scheme.equals("mdoc")) { - throw IllegalStateException("Only supports mdoc URIs") - } - val encodedReaderEngagement = Base64.decode( - uri.encodedSchemeSpecificPart, - Base64.URL_SAFE or Base64.NO_PADDING - ) - val engagement = EngagementParser( - encodedReaderEngagement - ).parse() - if (engagement.connectionMethods.size == 0) { - throw IllegalStateException("No connection methods in engagement") - } - - // For now, just pick the first transport - val connectionMethod = engagement.connectionMethods[0] - log("Using connection method $connectionMethod") - - val transport = DataTransport.fromConnectionMethod( - context, - connectionMethod, - DataTransport.Role.MDOC, - connectionSetup.getConnectionOptions() - ) - - val builder = DeviceRetrievalHelper.Builder( - context, - presentationListener, - context.mainExecutor(), - eDeviceKey - ) - builder.useReverseEngagement(transport, encodedReaderEngagement, origins) - presentation = builder.build() - onPresentationReady(requireNotNull(presentation)) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt deleted file mode 100644 index 01172abd7..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Color.BLACK -import android.graphics.Color.WHITE -import android.view.View -import android.widget.ImageView -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.android.identity.document.DocumentRequest -import com.android.identity.document.NameSpacedData -import com.android.identity.mdoc.mso.StaticAuthDataParser -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.mdoc.request.DeviceRequestParser -import com.android.identity.mdoc.response.DeviceResponseGenerator -import com.android.identity.mdoc.response.DocumentGenerator -import com.android.identity.mdoc.util.MdocUtil -import com.android.identity.crypto.Algorithm -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.KeyLockedException -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.documentdata.DocumentDataReader -import com.android.identity.wallet.documentdata.DocumentElements -import com.android.identity.wallet.util.ProvisioningUtil -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.util.requireValidProperty -import com.google.zxing.BarcodeFormat -import com.google.zxing.MultiFormatWriter -import com.google.zxing.WriterException -import com.google.zxing.common.BitMatrix -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.datetime.Clock -import kotlin.coroutines.resume - -class TransferManager private constructor(private val context: Context) { - - companion object { - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: TransferManager? = null - - fun getInstance(context: Context) = - instance ?: synchronized(this) { - instance ?: TransferManager(context).also { instance = it } - } - } - - private var reversedQrCommunicationSetup: ReverseQrCommunicationSetup? = null - private var qrCommunicationSetup: QrCommunicationSetup? = null - private var hasStarted = false - - private lateinit var communication: Communication - - private var transferStatusLd = MutableLiveData() - - fun setCommunication(communication: Communication) { - this.communication = communication - } - - fun getTransferStatus(): LiveData = transferStatusLd - - fun updateStatus(status: TransferStatus) { - transferStatusLd.value = status - } - - fun documentRequests(): Collection { - return communication.getDeviceRequest().docRequests - } - - fun startPresentationReverseEngagement( - reverseEngagementUri: String, - origins: List - ) { - if (hasStarted) { - throw IllegalStateException("Transfer has already started.") - } - communication = Communication.getInstance(context) - reversedQrCommunicationSetup = ReverseQrCommunicationSetup( - context = context, - onPresentationReady = { deviceRetrievalHelper -> - communication.deviceRetrievalHelper = deviceRetrievalHelper - }, - onNewRequest = { request -> - communication.setDeviceRequest(request) - transferStatusLd.value = TransferStatus.REQUEST - }, - onDisconnected = { transferStatusLd.value = TransferStatus.DISCONNECTED }, - onCommunicationError = { error -> - log("onError: ${error.message}") - transferStatusLd.value = TransferStatus.ERROR - } - ).apply { - configure(reverseEngagementUri, origins) - } - hasStarted = true - } - - fun startQrEngagement() { - if (hasStarted) { - throw IllegalStateException("Transfer has already started.") - } - communication = Communication.getInstance(context) - qrCommunicationSetup = QrCommunicationSetup( - context = context, - onConnecting = { transferStatusLd.value = TransferStatus.CONNECTING }, - onDeviceRetrievalHelperReady = { deviceRetrievalHelper -> - communication.deviceRetrievalHelper = deviceRetrievalHelper - transferStatusLd.value = TransferStatus.CONNECTED - }, - onNewDeviceRequest = { deviceRequest -> - communication.setDeviceRequest(deviceRequest) - transferStatusLd.value = TransferStatus.REQUEST - }, - onDisconnected = { transferStatusLd.value = TransferStatus.DISCONNECTED } - ) { error -> - log("onError: ${error.message}") - transferStatusLd.value = TransferStatus.ERROR - }.apply { - configure() - } - hasStarted = true - } - - fun getDeviceEngagementQrCode(): View { - val deviceEngagementForQrCode = qrCommunicationSetup!!.deviceEngagementUriEncoded - val qrCodeBitmap = encodeQRCodeAsBitmap(deviceEngagementForQrCode) - val qrCodeView = ImageView(context) - qrCodeView.setImageBitmap(qrCodeBitmap) - - return qrCodeView - } - - private fun encodeQRCodeAsBitmap(str: String): Bitmap { - val width = 800 - val result: BitMatrix = try { - MultiFormatWriter().encode( - str, - BarcodeFormat.QR_CODE, width, width, null - ) - } catch (e: WriterException) { - throw java.lang.IllegalArgumentException(e) - } - val w = result.width - val h = result.height - val pixels = IntArray(w * h) - for (y in 0 until h) { - val offset = y * w - for (x in 0 until w) { - pixels[offset + x] = if (result[x, y]) BLACK else WHITE - } - } - val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) - bitmap.setPixels(pixels, 0, width, 0, 0, w, h) - return bitmap - } - - @Throws(IllegalStateException::class) - suspend fun addDocumentToResponse( - documentName: String, - docType: String, - issuerSignedEntriesToRequest: MutableMap>, - deviceResponseGenerator: DeviceResponseGenerator, - credential: MdocCredential?, - authKeyUnlockData: KeyUnlockData?, - ) = suspendCancellableCoroutine { continuation -> - var result: AddDocumentToResponseResult - var signingKeyUsageLimitPassed = false - val documentManager = DocumentManager.getInstance(context) - val documentInformation = documentManager.getDocumentInformation(documentName) - requireValidProperty(documentInformation) { "Document not found!" } - - val document = requireNotNull(documentManager.getDocumentByName(documentName)) - val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key -> - issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value -> - DocumentRequest.DataElement(key, value, false) - } - } - - val request = DocumentRequest(dataElements) - - val credentialToUse: MdocCredential - if (credential != null) { - credentialToUse = credential - } else { - credentialToUse = document.findCredential( - ProvisioningUtil.CREDENTIAL_DOMAIN, - Clock.System.now() - ) as MdocCredential? - ?: throw IllegalStateException("No credential available") - } - - if (credentialToUse.usageCount >= documentInformation.maxUsagesPerKey) { - logWarning("Using Credential previously used ${credentialToUse.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") - signingKeyUsageLimitPassed = true - } - - val staticAuthData = StaticAuthDataParser(credentialToUse.issuerProvidedData).parse() - val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( - request, - document.applicationData.getNameSpacedData("documentData"), - staticAuthData - ) - - val transcript = communication.getSessionTranscript() ?: byteArrayOf() - - try { - val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) - .setIssuerNamespaces(mergedIssuerNamespaces) - val keyInfo = credentialToUse.secureArea.getKeyInfo(credentialToUse.alias) - if (keyInfo.keyPurposes.contains(KeyPurpose.SIGN)) { - generator.setDeviceNamespacesSignature( - NameSpacedData.Builder().build(), - credentialToUse.secureArea, - credentialToUse.alias, - authKeyUnlockData, - Algorithm.ES256 - ) - } else { - generator.setDeviceNamespacesMac( - NameSpacedData.Builder().build(), - credentialToUse.secureArea, - credentialToUse.alias, - authKeyUnlockData, - communication.deviceRetrievalHelper!!.eReaderKey - ) - } - val data = generator.generate() - deviceResponseGenerator.addDocument(data) - log("Increasing usage count on ${credentialToUse.alias}") - credentialToUse.increaseUsageCount() - ProvisioningUtil.getInstance(context).trackUsageTimestamp(document) - result = AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) - } catch (lockedException: KeyLockedException) { - result = AddDocumentToResponseResult.DocumentLocked(credentialToUse) - } - continuation.resume(result) - } - - fun stopPresentation( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - communication.stopPresentation( - sendSessionTerminationMessage, - useTransportSpecificSessionTermination - ) - disconnect() - } - - fun disconnect() { - communication.disconnect() - qrCommunicationSetup?.close() - transferStatusLd = MutableLiveData() - destroy() - } - - fun destroy() { - qrCommunicationSetup = null - reversedQrCommunicationSetup = null - hasStarted = false - } - - fun sendResponse(deviceResponse: ByteArray, closeAfterSending: Boolean) { - communication.sendResponse(deviceResponse, closeAfterSending) - if (closeAfterSending) { - disconnect() - } - } - - fun readDocumentEntries(documentName: String): DocumentElements { - val documentManager = DocumentManager.getInstance(context) - val documentInformation = documentManager.getDocumentInformation(documentName) - - val document = requireNotNull(documentManager.getDocumentByName(documentName)) - val nameSpacedData = document.applicationData.getNameSpacedData("documentData") - return DocumentDataReader(documentInformation?.docType ?: "").read(nameSpacedData) - } - - fun setResponseServed() { - transferStatusLd.value = TransferStatus.REQUEST_SERVED - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt deleted file mode 100644 index a1a1f829a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.identity.wallet.trustmanagement - -import java.security.cert.Certificate -import java.security.cert.CertificateException -import java.security.cert.PKIXCertPathChecker -import java.security.cert.X509Certificate - -/** - * Class used to validate that the country code in the whole certificate chain is the same - */ -class CountryValidator : PKIXCertPathChecker() { - private var previousCountryCode: String = "" - - /** - * There is no custom initialisation of this class - */ - override fun init(p0: Boolean) { - // intentionally left empty - } - - - /** - * Forward checking supported: the order of the certificate chain is not relevant for the check - * on country code. - */ - override fun isForwardCheckingSupported(): Boolean { - return true - } - - /** - * Check the country code - */ - override fun check(certificate: Certificate?, state: MutableCollection?) { - if (certificate is X509Certificate) { - val countryCode = certificate.subjectX500Principal.countryCode("") - if (countryCode.isBlank()) { - throw CertificateException("Country code is not present in certificate " + certificate.subjectX500Principal.name) - } - if (previousCountryCode.isNotBlank() && previousCountryCode.uppercase() != countryCode.uppercase()) { - throw CertificateException("There are different country codes in the certificate chain: $previousCountryCode and $countryCode") - } else { - previousCountryCode = countryCode - } - } - } - - /** - * Extensions are not validated on country code - */ - override fun getSupportedExtensions(): MutableSet { - return mutableSetOf() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt deleted file mode 100644 index aa5275971..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.identity.wallet.trustmanagement - -import java.security.cert.PKIXCertPathChecker - -/** - * Object used to obtain custom validators based on docType - */ -object CustomValidators { - fun getByDocType(docType: String): List - { - when (docType){ - "org.iso.18013.5.1.mDL" -> return listOf(CountryValidator()) - else -> return emptyList() - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt deleted file mode 100644 index 5b2f73741..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt +++ /dev/null @@ -1,63 +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.wallet.trustmanagement - -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle -import javax.security.auth.x500.X500Principal - -/** - * Extract the common name of a X500Principal (subject or issuer) - */ -fun X500Principal.getCommonName(defaultValue: String): String { - return readRdn(this.name, BCStyle.CN, defaultValue) -} - -/** - * Extract the organisation of a X500Principal (subject or issuer) - */ -fun X500Principal.getOrganisation(defaultValue: String): String { - return readRdn(this.name, BCStyle.O, defaultValue) -} - -/** - * Extract the organisational unit of a X500Principal (subject or issuer) - */ -fun X500Principal.organisationalUnit(defaultValue: String): String { - return readRdn(this.name, BCStyle.OU, defaultValue) -} - -/** - * Extract the country code of a X500Principal (subject or issuer) - */ -fun X500Principal.countryCode(defaultValue: String): String { - return readRdn(this.name, BCStyle.C, defaultValue) -} - -/** - * Read a relative distinguished name from a distinguished name - */ -private fun readRdn(name: String, field: ASN1ObjectIdentifier, defaultValue: String): String { - val x500name = X500Name(name) - for (rdn in x500name.getRDNs(field)) { - val attributes = rdn.typesAndValues - for (attribute in attributes) { - return attribute.value.toString() - } - } - return defaultValue -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt deleted file mode 100644 index fe32decec..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.android.identity.wallet.trustmanagement - -import com.android.identity.util.toHex -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier -import java.security.cert.X509Certificate - -/** - * Get the Subject Key Identifier Extension from the - * X509 certificate in hexadecimal format. - */ -fun X509Certificate.getSubjectKeyIdentifier(): String { - val extensionValue = this.getExtensionValue(Extension.subjectKeyIdentifier.id) - val octets = DEROctetString.getInstance(extensionValue).octets - val subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets) - return subjectKeyIdentifier.keyIdentifier.toHex() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt b/appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt deleted file mode 100644 index c61aba37b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.identity.wallet.util - -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.databinding.BindingAdapter - - -object BindingAdapters { - /** - * A Binding Adapter that is called whenever the value of the attribute `app:engagementView` - * changes. Receives a view with the QR Code for the device engagement. - */ - @BindingAdapter("app:engagementView") - @JvmStatic - fun engagementView(view: LinearLayout, viewEngagement: View?) { - viewEngagement?.let { - (viewEngagement.parent as? ViewGroup)?.removeView(viewEngagement) - view.addView(it) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt deleted file mode 100644 index 76ee921ec..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import android.os.Build -import androidx.core.content.ContextCompat -import java.util.concurrent.Executor - -fun Context.mainExecutor(): Executor { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - mainExecutor - } else { - ContextCompat.getMainExecutor(applicationContext) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt deleted file mode 100644 index 4bb0972c4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.util - -object DocumentData { - const val MDL_DOCTYPE = "org.iso.18013.5.1.mDL" - const val MDL_NAMESPACE = "org.iso.18013.5.1" - const val MVR_DOCTYPE = "nl.rdw.mekb.1" - const val MVR_NAMESPACE = "nl.rdw.mekb.1" - const val MICOV_DOCTYPE = "org.micov.1" - const val MICOV_VTR_NAMESPACE = "org.micov.vtr.1" - const val MICOV_ATT_NAMESPACE = "org.micov.attestation.1" - const val AAMVA_NAMESPACE = "org.iso.18013.5.1.aamva" - const val EU_PID_DOCTYPE = "eu.europa.ec.eudi.pid.1" - const val EU_PID_NAMESPACE = "eu.europa.ec.eudi.pid.1" - - enum class ErikaStaticData(val identifier: String, val value: String) { - VISIBLE_NAME("visible_name", "Driving License"), - } - - enum class MekbStaticData(val identifier: String, val value: String) { - VISIBLE_NAME("visible_name", "Vehicle Registration"), - } - - enum class MicovStaticData(val identifier: String, val value: String) { - VISIBLE_NAME("visible_name", "Vaccination Document"), - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt b/appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt deleted file mode 100644 index 179d0a179..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.android.identity.wallet.util - -import android.icu.text.SimpleDateFormat -import android.icu.util.TimeZone -import co.nstant.`in`.cbor.CborBuilder -import co.nstant.`in`.cbor.CborEncoder -import co.nstant.`in`.cbor.CborException -import co.nstant.`in`.cbor.model.DataItem -import java.io.ByteArrayOutputStream -import java.security.PublicKey -import java.security.interfaces.ECPublicKey -import java.security.spec.ECPoint -import kotlin.math.min - - -object FormatUtil { - // Helper function to convert a byteArray to HEX string - fun encodeToString(bytes: ByteArray): String { - val sb = StringBuilder(bytes.size * 2) - for (b in bytes) { - sb.append(String.format("%02x", b)) - } - - return sb.toString() - } - - private const val CHUNK_SIZE = 2048 - - private fun debugPrint(message: String) { - var index = 0 - while (index < message.length) { - log(message.substring(index, min(message.length, index + CHUNK_SIZE))) - index += CHUNK_SIZE - } - } - - fun debugPrintEncodeToString(bytes: ByteArray) { - debugPrint(encodeToString(bytes)) - } - - private const val COSE_KEY_KTY = 1 - private const val COSE_KEY_TYPE_EC2 = 2 - private const val COSE_KEY_EC2_CRV = -1 - private const val COSE_KEY_EC2_X = -2 - private const val COSE_KEY_EC2_Y = -3 - private const val COSE_KEY_EC2_CRV_P256 = 1 - - fun cborBuildCoseKey(key: PublicKey): DataItem { - val ecKey: ECPublicKey = key as ECPublicKey - val w: ECPoint = ecKey.w - // X and Y are always positive so for interop we remove any leading zeroes - // inserted by the BigInteger encoder. - val x = stripLeadingZeroes(w.affineX.toByteArray()) - val y = stripLeadingZeroes(w.affineY.toByteArray()) - return CborBuilder() - .addMap() - .put(COSE_KEY_KTY.toLong(), COSE_KEY_TYPE_EC2.toLong()) - .put(COSE_KEY_EC2_CRV.toLong(), COSE_KEY_EC2_CRV_P256.toLong()) - .put(COSE_KEY_EC2_X.toLong(), x) - .put(COSE_KEY_EC2_Y.toLong(), y) - .end() - .build()[0] - } - - fun cborEncode(dataItem: DataItem): ByteArray { - val baos = ByteArrayOutputStream() - try { - CborEncoder(baos).encode(dataItem) - } catch (e: CborException) { - // This should never happen and we don't want cborEncode() to throw since that - // would complicate all callers. Log it instead. - throw IllegalStateException("Unexpected failure encoding data", e) - } - return baos.toByteArray() - } - - private fun stripLeadingZeroes(value: ByteArray): ByteArray { - var n = 0 - while (n < value.size && value[n] == 0x00.toByte()) { - n++ - } - return value.copyOfRange(n, value.size) - } - - fun fullDateStringToMilliseconds(date: String): Long { - val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd") - simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") - return simpleDateFormat.parse(date).toInstant().toEpochMilli() - } - - fun millisecondsToFullDateString(milliseconds: Long): String { - val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd") - return simpleDateFormat.format(milliseconds) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt deleted file mode 100644 index 67e19216b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.android.identity.wallet.util - -import android.util.Log - -fun Any.log(message: String, exception: Throwable? = null) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - if (exception == null) { - Log.d(tag, message) - } else { - Log.e(tag, message, exception) - } -} - -fun Any.logInfo(message: String) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - Log.i(tag, message) -} - -fun Any.logWarning(message: String) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - Log.w(tag, message) -} - -fun Any.logError(message: String) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - Log.e(tag, message) -} - -private fun Any.tagValue(): String { - if (this is String) return this - val fullClassName: String = this::class.qualifiedName ?: this::class.java.typeName - val outerClassName = fullClassName.substringBefore('$') - val simplerOuterClassName = outerClassName.substringAfterLast('.') - return if (simplerOuterClassName.isEmpty()) { - fullClassName - } else { - simplerOuterClassName.removeSuffix("Kt") - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt b/appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt deleted file mode 100644 index ec7821753..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * 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.wallet.util - -import android.nfc.cardemulation.HostApduService -import android.os.Bundle -import com.android.identity.android.mdoc.transport.DataTransportNfc -import com.android.identity.wallet.transfer.TransferManager - -class NfcDataTransferHandler : HostApduService() { - - private lateinit var transferManager: TransferManager - - override fun onCreate() { - super.onCreate() - log("onCreate") - transferManager = TransferManager.getInstance(applicationContext) - } - - override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray? { - log("processCommandApdu: Command-> ${FormatUtil.encodeToString(commandApdu)}") - return DataTransportNfc.processCommandApdu(this, commandApdu) - } - - override fun onDeactivated(reason: Int) { - log("onDeactivated: reason-> $reason") - DataTransportNfc.onDeactivated(reason) - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt b/appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt deleted file mode 100644 index d52813e74..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * 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.wallet.util - -import android.content.Intent -import android.nfc.cardemulation.HostApduService -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import androidx.navigation.NavDeepLinkBuilder -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.android.mdoc.engagement.NfcEngagementHelper -import com.android.identity.android.mdoc.transport.DataTransport -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey -import com.android.identity.wallet.R -import com.android.identity.wallet.transfer.Communication -import com.android.identity.wallet.transfer.ConnectionSetup -import com.android.identity.wallet.transfer.TransferManager - -class NfcEngagementHandler : HostApduService() { - - private lateinit var engagementHelper: NfcEngagementHelper - private lateinit var communication: Communication - private lateinit var transferManager: TransferManager - - private var deviceRetrievalHelper: DeviceRetrievalHelper? = null - - private val settings by lazy { - PreferencesHelper.apply { initialize(applicationContext) } - } - private val eDeviceKey by lazy { - Crypto.createEcPrivateKey(settings.getEphemeralKeyCurveOption()) - } - private val nfcEngagementListener = object : NfcEngagementHelper.Listener { - - override fun onTwoWayEngagementDetected() { - log("Engagement Listener: Two Way Engagement Detected.") - } - - override fun onHandoverSelectMessageSent() { - log("Engagement Listener: Handover Select Message Sent.") - } - - override fun onDeviceConnecting() { - log("Engagement Listener: Device Connecting. Launching Transfer Screen") - val pendingIntent = NavDeepLinkBuilder(applicationContext) - .setGraph(R.navigation.navigation_graph) - .setDestination(R.id.transferDocumentFragment) - .setComponentName(com.android.identity.wallet.MainActivity::class.java) - .createPendingIntent() - pendingIntent.send(applicationContext, 0, null) - transferManager.updateStatus(TransferStatus.CONNECTING) - } - - override fun onDeviceConnected(transport: DataTransport) { - if (deviceRetrievalHelper != null) { - log("Engagement Listener: Device Connected -> ignored due to active presentation") - return - } - - log("Engagement Listener: Device Connected via NFC") - - val builder = DeviceRetrievalHelper.Builder( - applicationContext, - presentationListener, - applicationContext.mainExecutor(), - eDeviceKey - ) - builder.useForwardEngagement( - transport, - engagementHelper.deviceEngagement, - engagementHelper.handover - ) - deviceRetrievalHelper = builder.build() - communication.deviceRetrievalHelper = deviceRetrievalHelper - engagementHelper.close() - transferManager.updateStatus(TransferStatus.CONNECTED) - } - - override fun onError(error: Throwable) { - log("Engagement Listener: onError -> ${error.message}") - transferManager.updateStatus(TransferStatus.ERROR) - engagementHelper.close() - } - } - - private val presentationListener = object : DeviceRetrievalHelper.Listener { - - override fun onEReaderKeyReceived(eReaderKey: EcPublicKey) { - log("DeviceRetrievalHelper Listener (NFC): OnEReaderKeyReceived") - } - - override fun onDeviceRequest(deviceRequestBytes: ByteArray) { - log("Presentation Listener: OnDeviceRequest") - communication.setDeviceRequest(deviceRequestBytes) - transferManager.updateStatus(TransferStatus.REQUEST) - } - - override fun onDeviceDisconnected(transportSpecificTermination: Boolean) { - log("Presentation Listener: onDeviceDisconnected") - transferManager.updateStatus(TransferStatus.DISCONNECTED) - } - - override fun onError(error: Throwable) { - log("Presentation Listener: onError -> ${error.message}") - transferManager.updateStatus(TransferStatus.ERROR) - } - } - - override fun onCreate() { - super.onCreate() - log("onCreate") - communication = Communication.getInstance(applicationContext) - transferManager = TransferManager.getInstance(applicationContext) - transferManager.setCommunication(communication) - val connectionSetup = ConnectionSetup(applicationContext) - val builder = NfcEngagementHelper.Builder( - applicationContext, - eDeviceKey.publicKey, - connectionSetup.getConnectionOptions(), - nfcEngagementListener, - applicationContext.mainExecutor() - ) - if (PreferencesHelper.shouldUseStaticHandover()) { - builder.useStaticHandover(connectionSetup.getConnectionMethods()) - } else { - builder.useNegotiatedHandover() - } - engagementHelper = builder.build() - - val launchAppIntent = Intent(applicationContext, com.android.identity.wallet.MainActivity::class.java) - launchAppIntent.action = Intent.ACTION_VIEW - launchAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - launchAppIntent.addCategory(Intent.CATEGORY_DEFAULT) - launchAppIntent.addCategory(Intent.CATEGORY_BROWSABLE) - applicationContext.startActivity(launchAppIntent) - } - - override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray? { - log("processCommandApdu: Command-> ${FormatUtil.encodeToString(commandApdu)}") - return engagementHelper.nfcProcessCommandApdu(commandApdu) - } - - override fun onDeactivated(reason: Int) { - log("onDeactivated: reason-> $reason") - engagementHelper.nfcOnDeactivated(reason) - - // We need to close the NfcEngagementHelper but if we're doing it as the reader moves - // out of the field, it's too soon as it may take a couple of seconds to establish - // the connection, triggering onDeviceConnected() callback above. - // - // In fact, the reader _could_ actually take a while to establish the connection... - // for example the UI in the mdoc doc reader might have the operator pick the - // transport if more than one is offered. In fact this is exactly what we do in - // our mdoc reader. - // - // So we give the reader 15 seconds to do this... - // - val timeoutSeconds = 15 - Handler(Looper.getMainLooper()).postDelayed({ - if (deviceRetrievalHelper == null) { - logWarning("reader didn't connect inside $timeoutSeconds seconds, closing") - engagementHelper.close() - } - }, timeoutSeconds * 1000L) - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt b/appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt deleted file mode 100644 index 69e2c04b8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import java.util.concurrent.TimeUnit - -class PeriodicKeysRefreshWorkRequest(context: Context) { - - private val workManager = WorkManager.getInstance(context) - - fun schedulePeriodicKeysRefreshing() { - val workRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) - .build() - workManager.enqueue(workRequest) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt b/appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt deleted file mode 100644 index 096d4f7d5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt +++ /dev/null @@ -1,19 +0,0 @@ -@file:OptIn(ExperimentalContracts::class) - -package com.android.identity.wallet.util - -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -inline fun requireValidProperty(value: T?, lazyMessage: () -> Any): T { - contract { - returns() implies (value != null) - } - - if (value == null) { - val message = lazyMessage() - throw IllegalStateException(message.toString()) - } else { - return value - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt b/appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt deleted file mode 100644 index 90a36b64f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import androidx.preference.PreferenceManager -import com.android.identity.crypto.EcCurve -import com.android.identity.util.Logger -import java.io.File - -object PreferencesHelper { - private const val BLE_DATA_RETRIEVAL = "ble_transport" - private const val BLE_DATA_RETRIEVAL_PERIPHERAL_MODE = "ble_transport_peripheral_mode" - private const val BLE_DATA_L2CAP = "ble_l2cap" - private const val BLE_CLEAR_CACHE = "ble_clear_cache" - private const val WIFI_DATA_RETRIEVAL = "wifi_transport" - private const val NFC_DATA_RETRIEVAL = "nfc_transport" - private const val DEBUG_LOG = "debug_log" - private const val CONNECTION_AUTO_CLOSE = "connection_auto_close" - private const val STATIC_HANDOVER = "static_handover" - private const val EPHEMERAL_KEY_CURVE_OPTION = "ephemeral_key_curve" - - private lateinit var sharedPreferences: SharedPreferences - - fun initialize(context: Context) { - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - } - - fun getKeystoreBackedStorageLocation(context: Context): File { - // As per the docs, the document data contains reference to Keystore aliases so ensure - // this is stored in a location where it's not automatically backed up and restored by - // Android Backup as per https://developer.android.com/guide/topics/data/autobackup - return File(context.noBackupFilesDir, "identity.bin") - } - - fun isBleDataRetrievalEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_DATA_RETRIEVAL, true) - - fun setBleDataRetrievalEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_DATA_RETRIEVAL, enabled) } - - fun isBleDataRetrievalPeripheralModeEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_DATA_RETRIEVAL_PERIPHERAL_MODE, false) - - fun setBlePeripheralDataRetrievalMode(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_DATA_RETRIEVAL_PERIPHERAL_MODE, enabled) } - - fun isBleL2capEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_DATA_L2CAP, false) - - fun setBleL2CAPEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_DATA_L2CAP, enabled) } - - fun isBleClearCacheEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_CLEAR_CACHE, false) - - fun setBleClearCacheEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_CLEAR_CACHE, enabled) } - - fun isWifiDataRetrievalEnabled(): Boolean = - sharedPreferences.getBoolean(WIFI_DATA_RETRIEVAL, false) - - fun setWifiDataRetrievalEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(WIFI_DATA_RETRIEVAL, enabled) } - - fun isNfcDataRetrievalEnabled(): Boolean = - sharedPreferences.getBoolean(NFC_DATA_RETRIEVAL, false) - - fun setNfcDataRetrievalEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(NFC_DATA_RETRIEVAL, enabled) } - - fun isConnectionAutoCloseEnabled(): Boolean = - sharedPreferences.getBoolean(CONNECTION_AUTO_CLOSE, true) - - fun setConnectionAutoCloseEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(CONNECTION_AUTO_CLOSE, enabled) } - - fun shouldUseStaticHandover(): Boolean = - sharedPreferences.getBoolean(STATIC_HANDOVER, false) - - fun setUseStaticHandover(enabled: Boolean) = - sharedPreferences.edit { putBoolean(STATIC_HANDOVER, enabled) } - - fun isDebugLoggingEnabled(): Boolean = - sharedPreferences.getBoolean(DEBUG_LOG, true) - - fun setDebugLoggingEnabled(enabled: Boolean) = - sharedPreferences - .edit { putBoolean(DEBUG_LOG, enabled) } - .also { Logger.isDebugEnabled = enabled } - - fun getEphemeralKeyCurveOption(): EcCurve = - EcCurve.fromInt( - sharedPreferences.getInt( - EPHEMERAL_KEY_CURVE_OPTION, - EcCurve.P256.coseCurveIdentifier - ) - ) - - fun setEphemeralKeyCurveOption(newValue: EcCurve) = - sharedPreferences.edit { putInt(EPHEMERAL_KEY_CURVE_OPTION, newValue.coseCurveIdentifier) } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt b/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt deleted file mode 100644 index 04f3b55b4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt +++ /dev/null @@ -1,344 +0,0 @@ -package com.android.identity.wallet.util - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import com.android.identity.cbor.Bstr -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.Tagged -import com.android.identity.cbor.toDataItem -import com.android.identity.cose.Cose -import com.android.identity.cose.CoseNumberLabel -import com.android.identity.document.Document -import com.android.identity.document.DocumentUtil -import com.android.identity.document.NameSpacedData -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.EcCurve -import com.android.identity.crypto.toEcPrivateKey -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator -import com.android.identity.mdoc.mso.StaticAuthDataGenerator -import com.android.identity.mdoc.util.MdocUtil -import com.android.identity.securearea.SecureArea -import com.android.identity.securearea.SecureAreaRepository -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.KeysAndCertificates -import com.android.identity.wallet.selfsigned.ProvisionInfo -import com.android.identity.wallet.support.SecureAreaSupport -import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE -import com.android.identity.wallet.util.DocumentData.MVR_DOCTYPE -import java.io.ByteArrayOutputStream -import java.time.Instant as JavaInstant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import kotlin.random.Random -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant - -class ProvisioningUtil private constructor( - private val context: Context -) { - - val secureAreaRepository = SecureAreaRepository() - val documentStore by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { - HolderApp.createDocumentStore(context, secureAreaRepository) - } - - fun provisionSelfSigned( - nameSpacedData: NameSpacedData, - provisionInfo: ProvisionInfo, - ) { - val document = documentStore.createDocument(provisionInfo.documentName()) - documentStore.addDocument(document) - document.applicationData.setNameSpacedData("documentData", nameSpacedData) - - val authKeySecureArea: SecureArea = provisionInfo.currentSecureArea.secureArea - - // Store all settings for the document that are not SecureArea specific - document.applicationData.setString(USER_VISIBLE_NAME, provisionInfo.docName) - document.applicationData.setString(DOCUMENT_TYPE, provisionInfo.docType) - document.applicationData.setString(DATE_PROVISIONED, dateTimeFormatter.format(ZonedDateTime.now())) - document.applicationData.setNumber(CARD_ART, provisionInfo.docColor.toLong()) - document.applicationData.setBoolean(IS_SELF_SIGNED, true) - document.applicationData.setNumber(MAX_USAGES_PER_KEY, provisionInfo.maxUseMso.toLong()) - document.applicationData.setNumber(VALIDITY_IN_DAYS, provisionInfo.validityInDays.toLong()) - document.applicationData.setNumber(MIN_VALIDITY_IN_DAYS, provisionInfo.minValidityInDays.toLong()) - document.applicationData.setNumber(LAST_TIME_USED, -1) - document.applicationData.setString(AUTH_KEY_SECURE_AREA_IDENTIFIER, authKeySecureArea.identifier) - document.applicationData.setNumber(NUM_CREDENTIALS, provisionInfo.numberMso.toLong()) - - // Store settings for auth-key creation, these are all SecureArea-specific and we store - // them in a single blob at AUTH_KEY_SETTINGS - val support = SecureAreaSupport.getInstance(context, authKeySecureArea) - document.applicationData.setData( - AUTH_KEY_SETTINGS, - support.createAuthKeySettingsConfiguration(provisionInfo.secureAreaSupportState)) - - // Create initial batch of credentials - refreshCredentials(document, provisionInfo.docType) - } - - private fun ProvisionInfo.documentName(): String { - val regex = Regex("[^A-Za-z0-9 ]") - return regex.replace(docName, "").replace(" ", "_").lowercase() - } - - fun trackUsageTimestamp(document: Document) { - val now = Clock.System.now() - document.applicationData.setNumber(LAST_TIME_USED, now.toEpochMilliseconds()) - } - - fun refreshCredentials(document: Document, docType: String) { - val secureAreaIdentifier = document.applicationData.getString(AUTH_KEY_SECURE_AREA_IDENTIFIER) - val minValidTimeDays = document.applicationData.getNumber(MIN_VALIDITY_IN_DAYS) - val maxUsagesPerCred = document.applicationData.getNumber(MAX_USAGES_PER_KEY) - val numCreds = document.applicationData.getNumber(NUM_CREDENTIALS) - val validityInDays = document.applicationData.getNumber(VALIDITY_IN_DAYS).toInt() - - val now = Clock.System.now() - val validFrom = now - val validUntil = Instant.fromEpochMilliseconds( - validFrom.toEpochMilliseconds() + validityInDays*86400*1000L) - - val secureArea = secureAreaRepository.getImplementation(secureAreaIdentifier) - ?: throw IllegalStateException("No Secure Area with id ${secureAreaIdentifier} for document ${document.name}") - - val support = SecureAreaSupport.getInstance(context, secureArea) - val settings = support.createAuthKeySettingsFromConfiguration( - document.applicationData.getData(AUTH_KEY_SETTINGS), - "challenge".toByteArray(), - validFrom, - validUntil - ) - - val pendingCredsCount = DocumentUtil.managedCredentialHelper( - document, - CREDENTIAL_DOMAIN, - {toBeReplaced -> MdocCredential( - document, - toBeReplaced, - CREDENTIAL_DOMAIN, - secureArea, - settings, - docType - )}, - now, - numCreds.toInt(), - maxUsagesPerCred.toInt(), - minValidTimeDays*24*60*60*1000L, - false - ) - if (pendingCredsCount <= 0) { - return - } - - for (pendingCred in document.pendingCredentials.filter { it.domain == CREDENTIAL_DOMAIN }) { - pendingCred as MdocCredential - val msoGenerator = MobileSecurityObjectGenerator( - "SHA-256", - docType, - pendingCred.attestation.publicKey - ) - msoGenerator.setValidityInfo(now, validFrom, validUntil, null) - - // For mDLs, override the portrait with AuthenticationKeyCounter on top - // - var dataElementExceptions: Map>? = null - var dataElementOverrides: Map>? = null - if (docType.equals("org.iso.18013.5.1.mDL")) { - val portrait = document.applicationData.getNameSpacedData("documentData") - .getDataElementByteString("org.iso.18013.5.1", "portrait") - val portrait_override = overridePortrait(portrait, - pendingCred.credentialCounter) - - dataElementExceptions = - mapOf("org.iso.18013.5.1" to listOf("given_name", "portrait")) - dataElementOverrides = - mapOf("org.iso.18013.5.1" to mapOf( - "portrait" to Cbor.encode(Bstr(portrait_override)))) - } - - val issuerNameSpaces = MdocUtil.generateIssuerNameSpaces( - document.applicationData.getNameSpacedData("documentData"), - Random.Default, - 16, - dataElementOverrides - ) - - for (nameSpaceName in issuerNameSpaces.keys) { - val digests = MdocUtil.calculateDigestsForNameSpace( - nameSpaceName, - issuerNameSpaces, - Algorithm.SHA256 - ) - msoGenerator.addDigestIdsForNamespace(nameSpaceName, digests) - } - - val mso = msoGenerator.generate() - val taggedEncodedMso = Cbor.encode(Tagged(Tagged.ENCODED_CBOR, Bstr(mso))) - - val issuerKeyPair = when (docType) { - MVR_DOCTYPE -> KeysAndCertificates.getMekbDsKeyPair(context) - MICOV_DOCTYPE -> KeysAndCertificates.getMicovDsKeyPair(context) - else -> KeysAndCertificates.getMdlDsKeyPair(context) - } - - val issuerCert = when (docType) { - MVR_DOCTYPE -> KeysAndCertificates.getMekbDsCertificate(context) - MICOV_DOCTYPE -> KeysAndCertificates.getMicovDsCertificate(context) - else -> KeysAndCertificates.getMdlDsCertificate(context) - } - - val encodedIssuerAuth = Cbor.encode( - Cose.coseSign1Sign( - issuerKeyPair.private.toEcPrivateKey(issuerKeyPair.public, EcCurve.P256), - taggedEncodedMso, - true, - Algorithm.ES256, - protectedHeaders = mapOf( - Pair( - CoseNumberLabel(Cose.COSE_LABEL_ALG), - Algorithm.ES256.coseAlgorithmIdentifier.toDataItem() - ) - ), - unprotectedHeaders = mapOf( - Pair( - CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), - X509CertChain( - listOf(X509Cert(issuerCert.encoded)) - ).toDataItem() - ) - ), - ).toDataItem() - ) - - val issuerProvidedAuthenticationData = StaticAuthDataGenerator( - MdocUtil.stripIssuerNameSpaces(issuerNameSpaces, dataElementExceptions), - encodedIssuerAuth - ).generate() - - pendingCred.certify( - issuerProvidedAuthenticationData, - validFrom, - validUntil - ) - } - } - - // Puts the string "MSO ${counter}" on top of the portrait image. - private fun overridePortrait(encodedPortrait: ByteArray, counter: Number): ByteArray { - val options = BitmapFactory.Options() - options.inMutable = true - val bitmap = BitmapFactory.decodeByteArray( - encodedPortrait, - 0, - encodedPortrait.size, - options) - - val text = "MSO ${counter}" - val canvas = Canvas(bitmap) - val paint = Paint(Paint.ANTI_ALIAS_FLAG) - paint.setColor(Color.WHITE) - paint.textSize = bitmap.width / 5.0f - paint.setShadowLayer(2.0f, 1.0f, 1.0f, Color.BLACK) - val bounds = Rect() - paint.getTextBounds(text, 0, text.length, bounds) - val x: Float = (bitmap.width - bounds.width()) / 2.0f - val y: Float = (bitmap.height - bounds.height()) / 4.0f - canvas.drawText(text, x, y, paint) - - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos) - val encodedModifiedPortrait: ByteArray = baos.toByteArray() - - return encodedModifiedPortrait - } - - companion object { - - const val CREDENTIAL_DOMAIN = "mdoc/MSO" - private const val USER_VISIBLE_NAME = "userVisibleName" - const val DOCUMENT_TYPE = "documentType" - private const val DATE_PROVISIONED = "dateProvisioned" - private const val CARD_ART = "cardArt" - private const val IS_SELF_SIGNED = "isSelfSigned" - private const val MAX_USAGES_PER_KEY = "maxUsagesPerCredential" - private const val VALIDITY_IN_DAYS = "validityInDays" - private const val MIN_VALIDITY_IN_DAYS = "minValidityInDays" - private const val LAST_TIME_USED = "lastTimeUsed" - private const val NUM_CREDENTIALS = "numCredentials" - private const val AUTH_KEY_SETTINGS = "authKeySettings" - private const val AUTH_KEY_SECURE_AREA_IDENTIFIER = "authKeySecureAreaIdentifier" - - private val dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: ProvisioningUtil? = null - - fun getInstance(context: Context) = instance ?: synchronized(this) { - instance ?: ProvisioningUtil(context).also { instance = it } - } - - val defaultSecureArea: SecureArea - get() = requireNotNull(instance?.secureAreaRepository?.implementations?.first()) - - fun Document?.toDocumentInformation(): DocumentInformation? { - return this?.let { - - val authKeySecureAreaIdentifier = it.applicationData.getString(AUTH_KEY_SECURE_AREA_IDENTIFIER) - val authKeySecureArea = instance!!.secureAreaRepository.getImplementation(authKeySecureAreaIdentifier) - ?: throw IllegalStateException("No Secure Area with id ${authKeySecureAreaIdentifier} for document ${it.name}") - - val credentials = certifiedCredentials.map { key -> - key as MdocCredential - val info = authKeySecureArea.getKeyInfo(key.alias) - DocumentInformation.KeyData( - counter = key.credentialCounter.toInt(), - validFrom = key.validFrom.formatted(), - validUntil = key.validUntil.formatted(), - domain = key.domain, - issuerDataBytesCount = key.issuerProvidedData.size, - usagesCount = key.usageCount, - keyPurposes = info.keyPurposes.first(), - ecCurve = info.publicKey.curve, - isHardwareBacked = false, // TODO: remove - secureAreaDisplayName = authKeySecureArea.displayName - ) - } - val lastTimeUsedMillis = it.applicationData.getNumber(LAST_TIME_USED) - val lastTimeUsed = if (lastTimeUsedMillis == -1L) { - "" - } else { - Instant.fromEpochMilliseconds(lastTimeUsedMillis).formatted() - } - DocumentInformation( - userVisibleName = it.applicationData.getString(USER_VISIBLE_NAME), - docName = it.name, - docType = it.applicationData.getString(DOCUMENT_TYPE), - dateProvisioned = it.applicationData.getString(DATE_PROVISIONED), - documentColor = it.applicationData.getNumber(CARD_ART).toInt(), - selfSigned = it.applicationData.getBoolean(IS_SELF_SIGNED), - maxUsagesPerKey = it.applicationData.getNumber(MAX_USAGES_PER_KEY).toInt(), - lastTimeUsed = lastTimeUsed, - authKeys = credentials - ) - } - } - - private fun Instant.formatted(): String { - val javaInstant = JavaInstant.ofEpochMilli(this.toEpochMilliseconds()) - val dateTime = ZonedDateTime.ofInstant(javaInstant, ZoneId.systemDefault()) - return dateTimeFormatter.format(dateTime) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt b/appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt deleted file mode 100644 index c73520336..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.android.identity.wallet.document.DocumentManager - -class RefreshKeysWorker( - context: Context, - params: WorkerParameters -) : Worker(context, params) { - - private val documentManager = DocumentManager.getInstance(context) - private val provisioningUtil = ProvisioningUtil.getInstance(context) - - override fun doWork(): Result { - documentManager.getDocuments().forEach { documentInformation -> - val document = documentManager.getDocumentByName(documentInformation.docName) - document?.let { provisioningUtil.refreshCredentials(it, documentInformation.docType) } - } - return Result.success() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt b/appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt deleted file mode 100644 index 81f287011..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt +++ /dev/null @@ -1,342 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.wallet.R - -object SampleDataProvider { - const val MDL_NAMESPACE = "org.iso.18013.5.1" - const val AAMVA_NAMESPACE = "org.iso.18013.5.1.aamva" - const val MVR_NAMESPACE = "nl.rdw.mekb.1" - const val MICOV_ATT_NAMESPACE = "org.micov.attestation.1" - const val MICOV_VTR_NAMESPACE = "org.micov.vtr.1" - const val EUPID_NAMESPACE = "eu.europa.ec.eudi.pid.1" - - fun getSampleValue( - context: Context, - namespace: String, - identifier: String, - type: DocumentAttributeType, - identifierParent: String? = null - ): Any? { - return when (namespace) { - MDL_NAMESPACE -> when (identifier) { - "family_name" -> "Mustermann" - "given_name" -> "Erika" - "birth_date" -> "1971-09-01" - "issue_date" -> "2021-04-18" - "expiry_date" -> "2026-04-18" - "issuing_country" -> "US" - "issuing_authority" -> "Google" - "document_number" -> "987654321" - "portrait" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_portrait - ) - - "un_distinguishing_sign" -> "USA" - "administrative_number" -> "123456789" - "sex" -> 2 - "height" -> 175 - "weight" -> 68 - "eye_colour" -> "blue" - "hair_colour" -> "blond" - "birth_place" -> "Sample City" - "resident_address" -> "Sample address" - "portrait_capture_date" -> "2021-04-18" - "age_in_years" -> 52 - "age_birth_year" -> 1971 - "age_over_18" -> true - "age_over_21" -> true - "age_over_25" -> true - "age_over_62" -> false - "age_over_65" -> false - "issuing_jurisdiction" -> "Sample issuing jurisdiction" - "nationality" -> "US" - "resident_city" -> "Sample City" - "resident_state" -> "Sample State" - "resident_postal_code" -> "18013" - "resident_country" -> "US" - "family_name_national_character" -> "Бабіак" - "given_name_national_character" -> "Ерика" - "signature_usual_mark" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_signature - ) - - "biometric_template_face", - "biometric_template_finger", - "biometric_template_signature_sign", - "biometric_template_iris" -> Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) - - else -> defaultValue(type) - } - - AAMVA_NAMESPACE -> when (identifier) { - "name_suffix" -> "SR" - "organ_donor" -> 1 - "veteran" -> null - "family_name_truncation" -> "N" - "given_name_truncation" -> "N" - "aka_family_name" -> "Muster" - "aka_given_name" -> "Erik" - "aka_suffix" -> "JR" - "weight_range" -> 3 - "race_ethnicity" -> "W" - "DHS_compliance" -> "F" - "DHS_temporary_lawful_status" -> null - "EDL_credential" -> null - "resident_county" -> "123" - "hazmat_endorsement_expiration_date" -> "2026-04-18" - "sex" -> 2 - "audit_information" -> "Sample auditor" - "aamva_version" -> 2 - "domestic_vehicle_class_code" -> "B" - "domestic_vehicle_class_description" -> "Light vehicles" - "issue_date" -> "2021-04-18" - "expiry_date" -> "2026-04-18" - else -> defaultValue(type) - } - - MVR_NAMESPACE -> when (identifier) { - "issue_date" -> "2021-04-18" - "vin" -> "1M8GDM9AXKP042788" - "issuingCountry" -> "NL" - "competentAuthority" -> "RDW" - "registrationNumber" -> "E-01-23" - "validFrom" -> "2021-04-19" - "validUntil" -> "2023-04-20" - "ownershipStatus" -> 2 - "name" -> "Erika" - "streetName" -> "Teststraat" - "houseNumber" -> "86" - "houseNumberSuffix" -> "A" - "postalCode" -> "1234 AA" - "placeOfResidence" -> "Samplecity" - "make" -> "Dummymobile" - else -> defaultValue(type) - } - - MICOV_ATT_NAMESPACE -> when (identifier) { - "1D47_vaccinated" -> true - "RA01_vaccinated" -> true - "fac" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_portrait - ) - - "fni" -> "M" - "gni" -> "E" - "by" -> 1964 - "bm" -> 8 - "bd" -> 12 - "Result" -> "260415000" - "TypeOfTest" -> "LP6464-4" - "TimeOfTest" -> "2021-10-12" - "SeCondFulfilled" -> true - "SeCondType" -> "leisure" - "SeCondExpiry" -> "2021-10-13" - else -> defaultValue(type) - } - - MICOV_VTR_NAMESPACE -> when (identifier) { - "fn" -> "Mustermann" - "gn" -> "Erika" - "dob" -> "1964-08-12" - "sex" -> 2 - "tg" -> "840539006" - "vp" -> "1119349007" - "mp" -> "EU/1/20/1528" - "br" -> "Sample brand" - "ma" -> "ORG-100030215" - "bn" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> "B12345/67" - else -> "B67890/12" - } - - "dn" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> 1 - else -> 2 - } - - "sd" -> 2 - "dt" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> "2021-04-08" - else -> "2021-05-18" - } - - "co" -> "US" - "ao" -> "RHI" - "ap" -> "" - "nx" -> "2021-05-20" - "is" -> "SC17" - "ci" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> "URN:UVCI:01:UT:187/37512422923" - else -> "URN:UVCI:01:UT:187/37512533044" - } - - "pd" -> "" - "vf" -> "2021-05-27" - "vu" -> "2022-05-27" - "pty" -> when (identifierParent != null && identifierParent == "pid_PPN") { - true -> "PPN" - else -> "DL" - } - - "pnr" -> when (identifierParent != null && identifierParent == "pid_PPN") { - true -> "476284728" - else -> "987654321" - } - - "pic" -> "US" - "pia" -> "" - else -> defaultValue(type) - } - - EUPID_NAMESPACE -> when (identifier) { - "family_name" -> "Mustermann" - "family_name_national_characters" -> "Бабіак" - "given_name" -> "Erika" - "given_name_national_characters" -> "Ерика" - "birth_date" -> "1986-03-14" - "persistent_id" -> "0128196532" - "family_name_birth" -> "Mustermann" - "family_name_birth_national_characters" -> "Бабіак" - "given_name_birth" -> "Erika" - "given_name_birth_national_characters" -> "Ерика" - "birth_place" -> "Place of birth" - "resident_address" -> "Resident address" - "resident_city" -> "Resident City" - "resident_postal_code" -> "Resident postal code" - "resident_state" -> "Resident state" - "resident_country" -> "Resident country" - "gender" -> "female" - "nationality" -> "NL" - "portrait" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_portrait - ) - "portrait_capture_date" -> "2022-11-14" - "biometric_template_finger" -> Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) - "age_over_13" -> true - "age_over_16" -> true - "age_over_18" -> true - "age_over_21" -> true - "age_over_60" -> false - "age_over_65" -> false - "age_over_68" -> false - "age_in_years" -> 37 - "age_birth_year" -> 1986 - else -> defaultValue(type) - } - - else -> defaultValue(type) - } - } - - fun getSampleValue(namespace: String, identifier: String, type: DocumentAttributeType, index: Int): Any? { - return when (namespace) { - MDL_NAMESPACE -> when (identifier) { - "vehicle_category_code" -> when (index) { - 0 -> "A" - else -> "B" - } - - "issue_date" -> when (index) { - 0 -> "2018-08-09" - else -> "2017-02-23" - } - - "expiry_date" -> when (index) { - 0 -> "2024-10-20" - else -> "2024-10-20" - } - - "code" -> when (index) { - 0 -> "S01" - else -> "S02" - } - - "sign" -> when (index) { - 0 -> "<=" - else -> "=" - } - - "value" -> when (index) { - 0 -> "2500" - else -> "8" - } - - else -> defaultValue(type) - } - - AAMVA_NAMESPACE -> when (identifier) { - "domestic_vehicle_restriction_code" -> when (index) { - 0 -> "B" - else -> "C" - } - - "domestic_vehicle_restriction_description" -> when (index) { - 0 -> "Corrective lenses must be worn" - else -> "Mechanical Aid (special brakes, hand controls, or other adaptive devices)" - } - - "domestic_vehicle_endorsement_code" -> when (index) { - 0 -> "P" - else -> "S" - } - - "domestic_vehicle_endorsement_description" -> when (index) { - 0 -> "Passenger" - else -> "School Bus" - } - - else -> defaultValue(type) - } - - else -> defaultValue(type) - } - } - - fun getArrayLength(namespace: String, identifier: String): Int { - return when (namespace) { - MDL_NAMESPACE -> when (identifier) { - "driving_privileges" -> 2 - "codes" -> 2 - else -> 2 - } - - AAMVA_NAMESPACE -> when (identifier) { - "domestic_driving_privileges" -> 1 - "domestic_vehicle_restrictions" -> 2 - "domestic_vehicle_endorsements" -> 2 - else -> 2 - } - - else -> 2 - } - } - - private fun defaultValue(type: DocumentAttributeType): Any? { - return when (type) { - is DocumentAttributeType.Blob -> byteArrayOf() - is DocumentAttributeType.String -> "-" - is DocumentAttributeType.Number -> 0 - is DocumentAttributeType.Date, - is DocumentAttributeType.DateTime -> "2100-01-01" - - is DocumentAttributeType.Picture -> Bitmap.createBitmap( - 200, - 200, - Bitmap.Config.ARGB_8888 - ) - - is DocumentAttributeType.Boolean -> false - is DocumentAttributeType.StringOptions, - is DocumentAttributeType.IntegerOptions, - is DocumentAttributeType.ComplexType -> null - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt deleted file mode 100644 index 2fc445ce0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.util - -import androidx.lifecycle.SavedStateHandle - -fun SavedStateHandle.updateState(block: (T) -> T) { - val prevValue = get("state")!! - val nextValue = block(prevValue) - set("state", nextValue) -} - -fun SavedStateHandle.getState(initialState: T) = getStateFlow("state", initialState) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt b/appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt deleted file mode 100644 index c41943ef5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.util - -enum class TransferStatus { - ENGAGEMENT_DETECTED, - CONNECTING, - CONNECTED, - REQUEST, - REQUEST_SERVED, - DISCONNECTED, - ERROR -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt b/appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt deleted file mode 100644 index a4f72f9ff..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.android.identity.wallet.util - -import android.graphics.Bitmap -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.IntegerOption -import com.android.identity.documenttype.StringOption - -data class Field( - val id: Int, - val label: String, - val name: String, - val fieldType: DocumentAttributeType, - val value: Any?, - val namespace: String? = null, - val isArray: Boolean = false, - val parentId: Int? = null, - var stringOptions: List? = null, - var integerOptions: List? = null -) { - fun hasValue(): Boolean { - return value != "" - } - - fun getValueLong(): Long { - return value?.toString()?.toLong() ?: 0 - } - - fun getValueString(): String { - return value as String - } - - fun getValueBoolean(): Boolean { - return value as Boolean - } - - fun getValueBitmap(): Bitmap { - return value as Bitmap - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt deleted file mode 100644 index afb48c285..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt +++ /dev/null @@ -1,286 +0,0 @@ -package com.android.identity.wallet.viewmodel - -import android.app.Application -import android.view.View -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.MdocDocumentType -import com.android.identity.documenttype.MdocDataElement -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.documentdata.MdocComplexTypeDefinition -import com.android.identity.wallet.documentdata.MdocComplexTypeRepository -import com.android.identity.wallet.selfsigned.SelfSignedDocumentData -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.util.SampleDataProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class SelfSignedViewModel(val app: Application) : - AndroidViewModel(app) { - - companion object { - private const val LOG_TAG = "SelfSignedViewModel" - } - - private val documentManager = DocumentManager.getInstance(app.applicationContext) - val loading = MutableLiveData() - val created = MutableLiveData() - private val fieldsByDocType: MutableMap> = mutableMapOf() - private var id = 1 - - init { - loading.value = View.GONE - for (documentType in HolderApp.documentTypeRepositoryInstance.documentTypes - .filter { it.mdocDocumentType != null }) { - id = 1 // reset the id to 1 - fieldsByDocType[documentType.mdocDocumentType?.docType!!] = - createFields(documentType.mdocDocumentType!!) - } - } - - fun getFields(docType: String): MutableList { - return fieldsByDocType[docType] - ?: throw IllegalArgumentException("No field list valid for $docType") - } - - fun createSelfSigned(documentData: SelfSignedDocumentData) { - loading.value = View.VISIBLE - viewModelScope.launch { - withContext(Dispatchers.IO) { - documentManager.createSelfSignedDocument(documentData) - } - created.value = true - loading.value = View.GONE - } - } - - private fun createFields(mdocDocumentType: MdocDocumentType): MutableList { - val fields: MutableList = mutableListOf() - val complexTypes = MdocComplexTypeRepository.getComplexTypes(mdocDocumentType.docType) - for (namespace in mdocDocumentType.namespaces.values) { - val namespaceComplexTypes = - complexTypes?.namespaces?.find { it.namespace == namespace.namespace } - for (dataElement in namespace.dataElements.values) { - when (dataElement.attribute.type) { - is DocumentAttributeType.ComplexType -> { - val complexTypeDefinitions = namespaceComplexTypes?.dataElements?.filter { - it.parentIdentifiers.contains(dataElement.attribute.identifier) - } - - if (complexTypeDefinitions?.first()?.partOfArray == true) { - val arrayLength = - SampleDataProvider.getArrayLength( - namespace.namespace, - dataElement.attribute.identifier - ) - val parentField = Field( - id++, - "${dataElement.attribute.displayName} ($arrayLength items)", - dataElement.attribute.identifier, - dataElement.attribute.type, - null, - namespace = namespace.namespace, - isArray = true, - ) - fields.add(parentField) - addArrayFields( - parentField, - fields, - namespaceComplexTypes.dataElements) - } else { - val parentField = Field( - id++, - dataElement.attribute.displayName, - dataElement.attribute.identifier, - dataElement.attribute.type, - null, - namespace = namespace.namespace - ) - fields.add(parentField) - addMapFields( - parentField, - fields, - namespaceComplexTypes?.dataElements!!) - } - } - - else -> { - - val sampleValue = SampleDataProvider.getSampleValue( - app, - namespace.namespace, - dataElement.attribute.identifier, - dataElement.attribute.type - ) - val field = Field( - id++, - dataElement.attribute.displayName, - dataElement.attribute.identifier, - dataElement.attribute.type, - sampleValue, - namespace = namespace.namespace - ) - addOptions(field, dataElement) - fields.add(field) - } - } - } - } - return fields - } - - - private fun addArrayFields( - parentField: Field, - fields: MutableList, - dataElements: List, - prefix: String = "" - ) { - val arrayLength = - SampleDataProvider.getArrayLength(parentField.namespace!!, parentField.name) - val childElements = dataElements.filter { it.parentIdentifiers.contains(parentField.name) } - for (i in 0..arrayLength - 1) { - for (childElement in childElements) { - if (childElement.type is DocumentAttributeType.ComplexType) { - - if (dataElements.any { it.parentIdentifiers.contains(childElement.identifier) && it.partOfArray }) { - val childField = Field( - id++, - "$prefix${i + 1} | ${childElement.displayName} (${ - SampleDataProvider.getArrayLength( - parentField.namespace, - childElement.identifier - ) - } items)", - childElement.identifier, - childElement.type, - null, - namespace = parentField.namespace, - isArray = true, - parentId = parentField.id - ) - fields.add(childField) - addArrayFields( - childField, - fields, - dataElements, - "$prefix${i + 1} | " - ) - } else { - val childField = Field( - id++, - "$prefix${i + 1} | ${childElement.displayName}", - childElement.identifier, - childElement.type, - null, - namespace = parentField.namespace, - parentId = parentField.id - ) - fields.add(childField) - addMapFields( - childField, - fields, - dataElements, - "$prefix${i + 1} | " - ) - } - } else { - val sampleValue = - SampleDataProvider.getSampleValue(parentField.namespace, childElement.identifier, childElement.type, i) - val childField = Field( - id++, - "$prefix${i + 1} | ${childElement.displayName}", - childElement.identifier, - childElement.type, - sampleValue, - namespace = parentField.namespace, - parentId = parentField.id - ) - addOptions(childField, childElement) - fields.add(childField) - } - } - } - } - - private fun addMapFields( - parentField: Field, - fields: MutableList, - dataElements: List, - prefix: String = "" - ) { - - val childElements = dataElements.filter { it.parentIdentifiers.contains(parentField.name) } - for (childElement in childElements) { - if (childElement.type is DocumentAttributeType.ComplexType) { - val isArray = dataElements.any { it.parentIdentifiers.contains(childElement.identifier) && it.partOfArray } - val childField = Field( - id++, - "$prefix${childElement.displayName}", - childElement.identifier, - childElement.type, - null, - namespace = parentField.namespace, - isArray = isArray, - parentId = parentField.id - ) - fields.add(childField) - if (isArray){ - addArrayFields(childField, fields, dataElements, prefix) - } else { - addMapFields(childField, fields, dataElements, prefix) - } - } else { - val sampleValue = - SampleDataProvider.getSampleValue( - app, - parentField.namespace!!, - childElement.identifier, - childElement.type - ) - val childField = Field( - id++, - "$prefix${childElement.displayName}", - childElement.identifier, - childElement.type, - sampleValue, - namespace = parentField.namespace, - parentId = parentField.id - ) - addOptions(childField, childElement) - fields.add(childField) - } - - } - } - - fun addOptions(field: Field, dataElement: MdocDataElement) { - when (dataElement.attribute.type) { - is DocumentAttributeType.StringOptions -> field.stringOptions = - (dataElement.attribute.type as DocumentAttributeType.StringOptions).options - - is DocumentAttributeType.IntegerOptions -> field.integerOptions = - (dataElement.attribute.type as DocumentAttributeType.IntegerOptions).options - - else -> {} - } - } - - fun addOptions(field: Field, dataElement: MdocComplexTypeDefinition) { - when (dataElement.type) { - is DocumentAttributeType.StringOptions -> field.stringOptions = - dataElement.type.options - - is DocumentAttributeType.IntegerOptions -> field.integerOptions = - dataElement.type.options - - else -> {} - } - } - -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt deleted file mode 100644 index 8e98d874c..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.android.identity.wallet.viewmodel - -import android.app.Application -import android.view.View -import androidx.databinding.ObservableField -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.wallet.transfer.TransferManager -import com.android.identity.wallet.util.TransferStatus - -class ShareDocumentViewModel(val app: Application) : AndroidViewModel(app) { - - private val transferManager = TransferManager.getInstance(app.applicationContext) - var deviceEngagementQr = ObservableField() - var message = ObservableField() - private var hasStarted = false - - fun getTransferStatus(): LiveData = transferManager.getTransferStatus() - - fun startPresentationReverseEngagement( - reverseEngagementUri: String, - originInfos: List - ) { - if (!hasStarted) { - transferManager.startPresentationReverseEngagement(reverseEngagementUri, originInfos) - hasStarted = true - } - } - - fun cancelPresentation() { - transferManager.stopPresentation( - sendSessionTerminationMessage = true, - useTransportSpecificSessionTermination = false - ) - hasStarted = false - message.set("Presentation canceled") - } - - fun showQrCode() { - deviceEngagementQr.set(transferManager.getDeviceEngagementQrCode()) - } - - fun triggerQrEngagement() { - transferManager.startQrEngagement() - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt deleted file mode 100644 index cbe4f224b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.android.identity.wallet.viewmodel - -import android.app.Application -import android.view.View -import androidx.databinding.ObservableField -import androidx.databinding.ObservableInt -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.mdoc.request.DeviceRequestParser -import com.android.identity.mdoc.response.DeviceResponseGenerator -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.util.Constants.DEVICE_RESPONSE_STATUS_OK -import com.android.identity.wallet.R -import com.android.identity.wallet.authconfirmation.RequestedDocumentData -import com.android.identity.wallet.authconfirmation.RequestedElement -import com.android.identity.wallet.authconfirmation.SignedElementsCollection -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.transfer.AddDocumentToResponseResult -import com.android.identity.wallet.transfer.TransferManager -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.logWarning -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { - - private val transferManager = TransferManager.getInstance(app.applicationContext) - private val documentManager = DocumentManager.getInstance(app.applicationContext) - private val signedElements = SignedElementsCollection() - private val requestedElements = mutableListOf() - private val closeConnectionMutableLiveData = MutableLiveData() - private val selectedDocuments = mutableListOf() - - var inProgress = ObservableInt(View.GONE) - var documentsSent = ObservableField() - val connectionClosedLiveData: LiveData = closeConnectionMutableLiveData - - private val mutableConfirmationState = MutableLiveData() - val authConfirmationState: LiveData = mutableConfirmationState - - fun onAuthenticationCancelled() { - mutableConfirmationState.value = true - } - - fun onAuthenticationCancellationConsumed() { - mutableConfirmationState.value = null - } - - fun getTransferStatus(): LiveData = - transferManager.getTransferStatus() - - fun getRequestedDocuments(): Collection = - transferManager.documentRequests() - - fun getDocuments() = documentManager.getDocuments() - - fun getSelectedDocuments() = selectedDocuments - - fun requestedElements() = requestedElements - - fun closeConnection() { - cleanUp() - closeConnectionMutableLiveData.value = true - } - - fun addDocumentForSigning(document: RequestedDocumentData) { - signedElements.addNamespace(document) - } - - fun toggleSignedElement(element: RequestedElement) { - signedElements.toggleProperty(element) - } - - fun createSelectedItemsList() { - val ownDocuments = getSelectedDocuments() - val requestedDocuments = getRequestedDocuments() - val result = mutableListOf() - requestedDocuments.forEach { requestedDocument -> - try { - val ownDocument = ownDocuments.first { it.docType == requestedDocument.docType } - val issuerSignedEntriesToRequest = requestedElementsFrom(requestedDocument) - result.add( - RequestedDocumentData( - userReadableName = ownDocument.userVisibleName, - identityCredentialName = ownDocument.docName, - requestedElements = issuerSignedEntriesToRequest, - requestedDocument = requestedDocument - ) - ) - } catch (e: NoSuchElementException) { - logWarning("No document for docType " + requestedDocument.docType) - } - } - requestedElements.addAll(result) - } - - fun sendResponseForSelection( - onResultReady: (result: AddDocumentToResponseResult) -> Unit, - credential: MdocCredential? = null, - authKeyUnlockData: KeyUnlockData? = null - ) { - val elementsToSend = signedElements.collect() - val responseGenerator = DeviceResponseGenerator(DEVICE_RESPONSE_STATUS_OK) - viewModelScope.launch { - elementsToSend.forEach { signedDocument -> - try { - val issuerSignedEntries = signedDocument.issuerSignedEntries() - val result = withContext(Dispatchers.IO) { //<- Offload from UI thread - transferManager.addDocumentToResponse( - signedDocument.identityCredentialName, - signedDocument.documentType, - issuerSignedEntries, - responseGenerator, - credential, - authKeyUnlockData, - ) - } - if (result !is AddDocumentToResponseResult.DocumentAdded) { - onResultReady(result) - return@forEach - } - transferManager.sendResponse( - responseGenerator.generate(), - PreferencesHelper.isConnectionAutoCloseEnabled() - ) - transferManager.setResponseServed() - val documentsCount = elementsToSend.count() - documentsSent.set(app.getString(R.string.txt_documents_sent, documentsCount as Int)) - cleanUp() - onResultReady(result) - /* - } catch (e: CredentialInvalidatedException) { - logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.") - documentManager.deleteCredentialByName(signedDocument.identityCredentialName) - Toast.makeText( - app.applicationContext, "Deleting invalid document " - + signedDocument.identityCredentialName, - Toast.LENGTH_SHORT - ).show() - */ - } catch (e: NoSuchElementException) { - logWarning("No requestedDocument for " + signedDocument.documentType) - } - } - } - } - - fun cancelPresentation( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - transferManager.stopPresentation( - sendSessionTerminationMessage, - useTransportSpecificSessionTermination - ) - } - - private fun requestedElementsFrom( - requestedDocument: DeviceRequestParser.DocRequest - ): ArrayList { - val result = arrayListOf() - requestedDocument.namespaces.forEach { namespace -> - val elements = requestedDocument.getEntryNames(namespace).map { element -> - RequestedElement(namespace, element) - } - result.addAll(elements) - } - return result - } - - private fun cleanUp() { - requestedElements.clear() - signedElements.clear() - selectedDocuments.clear() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt b/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt deleted file mode 100644 index ab7f3ac23..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.wallet - -import android.content.Context -import android.view.View -import androidx.viewpager2.widget.ViewPager2 -import com.android.identity.wallet.R -import kotlin.math.abs - -class DocumentPageTransformer(context: Context) : ViewPager2.PageTransformer { - private val resources = context.resources - private val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible) - private val currentItemHorizontalMarginPx = resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin) - private val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx - - override fun transformPage(page: View, position: Float) { - page.translationX = -pageTranslationX * position - page.scaleY = 1 - (0.25f * abs(position)) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt b/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt deleted file mode 100644 index c9903b79d..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.wallet - -import android.content.Context -import android.graphics.Rect -import android.view.View -import androidx.annotation.DimenRes -import androidx.recyclerview.widget.RecyclerView - -class DocumentPagerItemDecoration( - context: Context, - @DimenRes horizontalMarginInDp: Int -) : RecyclerView.ItemDecoration() { - - private val horizontalMarginInPx: Int = - context.resources.getDimension(horizontalMarginInDp).toInt() - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - outRect.right = horizontalMarginInPx - outRect.left = horizontalMarginInPx - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt deleted file mode 100644 index 14ad18614..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.android.identity.wallet.wallet - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.R -import com.android.identity.wallet.adapter.DocumentAdapter -import com.android.identity.wallet.databinding.FragmentSelectDocumentBinding -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.log -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel -import com.google.android.material.tabs.TabLayoutMediator - -class SelectDocumentFragment : Fragment() { - - private var _binding: FragmentSelectDocumentBinding? = null - private val binding get() = _binding!! - - private val viewModel: ShareDocumentViewModel by activityViewModels() - private val timeInterval = 2000 // # milliseconds passed between two back presses - private var mBackPressed: Long = 0 - - private val appPermissions: Array = - if (android.os.Build.VERSION.SDK_INT >= 31) { - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.BLUETOOTH_ADVERTISE, - Manifest.permission.BLUETOOTH_SCAN, - Manifest.permission.BLUETOOTH_CONNECT, - ) - } else { - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - ) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // Ask to press twice before leave the app - requireActivity().onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (mBackPressed + timeInterval > System.currentTimeMillis()) { - requireActivity().finish() - return - } else { - Toast.makeText( - requireContext(), - R.string.toast_press_back_twice, - Toast.LENGTH_SHORT - ).show() - } - mBackPressed = System.currentTimeMillis() - } - }) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSelectDocumentBinding.inflate(inflater) - val adapter = DocumentAdapter() - binding.vpDocuments.adapter = adapter - binding.fragment = this - setupDocumentsPager(binding) - - val documentManager = DocumentManager.getInstance(requireContext()) - setupScreen(binding, adapter, documentManager.getDocuments().toMutableList()) - - val permissionsNeeded = appPermissions.filter { permission -> - ContextCompat.checkSelfPermission( - requireContext(), - permission - ) != PackageManager.PERMISSION_GRANTED - } - - if (permissionsNeeded.isNotEmpty()) { - permissionsLauncher.launch( - permissionsNeeded.toTypedArray() - ) - } - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - viewModel.getTransferStatus().observe(viewLifecycleOwner) { - when (it) { - TransferStatus.CONNECTED -> { - openTransferScreen() - } - - TransferStatus.ERROR -> { - binding.tvNfcLabel.text = "Error on presentation!" - } - //Shall we update the top label of the screen for each state? - else -> {} - } - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun setupDocumentsPager(binding: FragmentSelectDocumentBinding) { - TabLayoutMediator(binding.tlPageIndicator, binding.vpDocuments) { _, _ -> }.attach() - binding.vpDocuments.offscreenPageLimit = 1 - binding.vpDocuments.setPageTransformer(DocumentPageTransformer(requireContext())) - val itemDecoration = DocumentPagerItemDecoration( - requireContext(), - R.dimen.viewpager_current_item_horizontal_margin - ) - binding.vpDocuments.addItemDecoration(itemDecoration) - } - - private fun setupScreen( - binding: FragmentSelectDocumentBinding, - adapter: DocumentAdapter, - documentsList: MutableList - ) { - if (documentsList.isEmpty()) { - showEmptyView(binding) - } else { - adapter.submitList(documentsList) - showDocumentsPager(binding) - } - } - - private fun openTransferScreen() { - val destination = SelectDocumentFragmentDirections.toTransferDocument() - findNavController().navigate(destination) - } - - private fun showEmptyView(binding: FragmentSelectDocumentBinding) { - binding.vpDocuments.visibility = View.GONE - binding.cvEmptyView.visibility = View.VISIBLE - binding.btShowQr.visibility = View.GONE - binding.btAddDocument.setOnClickListener { openAddDocument() } - } - - private fun showDocumentsPager(binding: FragmentSelectDocumentBinding) { - binding.vpDocuments.visibility = View.VISIBLE - binding.cvEmptyView.visibility = View.GONE - binding.btShowQr.visibility = View.VISIBLE - binding.btShowQr.setOnClickListener { displayQRCode() } - } - - private fun displayQRCode() { - val destination = SelectDocumentFragmentDirections.toShowQR() - findNavController().navigate(destination) - } - - private fun openAddDocument() { - val destination = SelectDocumentFragmentDirections.toAddSelfSigned() - findNavController().navigate(destination) - } - - private val permissionsLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - log("permissionsLauncher ${it.key} = ${it.value}") - if (!it.value) { - Toast.makeText( - activity, - "The ${it.key} permission is required for BLE", - Toast.LENGTH_LONG - ).show() - return@registerForActivityResult - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/res/drawable/blue_gradient.xml b/appholder/src/main/res/drawable/blue_gradient.xml deleted file mode 100644 index 25fb5fc4c..000000000 --- a/appholder/src/main/res/drawable/blue_gradient.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/bottom_sheet_handle.xml b/appholder/src/main/res/drawable/bottom_sheet_handle.xml deleted file mode 100644 index 3e1969557..000000000 --- a/appholder/src/main/res/drawable/bottom_sheet_handle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/default_page_dot.xml b/appholder/src/main/res/drawable/default_page_dot.xml deleted file mode 100644 index 03c1edb14..000000000 --- a/appholder/src/main/res/drawable/default_page_dot.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/driving_license_bg.png b/appholder/src/main/res/drawable/driving_license_bg.png deleted file mode 100644 index 180488d12..000000000 Binary files a/appholder/src/main/res/drawable/driving_license_bg.png and /dev/null differ diff --git a/appholder/src/main/res/drawable/gradient_red.xml b/appholder/src/main/res/drawable/gradient_red.xml deleted file mode 100644 index 725371076..000000000 --- a/appholder/src/main/res/drawable/gradient_red.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/green_gradient.xml b/appholder/src/main/res/drawable/green_gradient.xml deleted file mode 100644 index 44e42bf07..000000000 --- a/appholder/src/main/res/drawable/green_gradient.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/ic_add_document.xml b/appholder/src/main/res/drawable/ic_add_document.xml deleted file mode 100644 index 7bf1f4a44..000000000 --- a/appholder/src/main/res/drawable/ic_add_document.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/ic_add_http.xml b/appholder/src/main/res/drawable/ic_add_http.xml deleted file mode 100644 index 283b25e12..000000000 --- a/appholder/src/main/res/drawable/ic_add_http.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/appholder/src/main/res/drawable/ic_launcher_foreground.xml b/appholder/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 04aff291b..000000000 --- a/appholder/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/ic_nfc.xml b/appholder/src/main/res/drawable/ic_nfc.xml deleted file mode 100644 index 075742a4e..000000000 --- a/appholder/src/main/res/drawable/ic_nfc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/appholder/src/main/res/drawable/ic_outline_info_24.xml b/appholder/src/main/res/drawable/ic_outline_info_24.xml deleted file mode 100644 index 35f7f5f61..000000000 --- a/appholder/src/main/res/drawable/ic_outline_info_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/appholder/src/main/res/drawable/ic_present_document.xml b/appholder/src/main/res/drawable/ic_present_document.xml deleted file mode 100644 index 91f8971aa..000000000 --- a/appholder/src/main/res/drawable/ic_present_document.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/appholder/src/main/res/drawable/ic_settings.xml b/appholder/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index 1134c3184..000000000 --- a/appholder/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/ic_wallet.xml b/appholder/src/main/res/drawable/ic_wallet.xml deleted file mode 100644 index 94ca58094..000000000 --- a/appholder/src/main/res/drawable/ic_wallet.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/img_erika_portrait.jpg b/appholder/src/main/res/drawable/img_erika_portrait.jpg deleted file mode 100644 index 31e356ddc..000000000 Binary files a/appholder/src/main/res/drawable/img_erika_portrait.jpg and /dev/null differ diff --git a/appholder/src/main/res/drawable/img_erika_signature.jpg b/appholder/src/main/res/drawable/img_erika_signature.jpg deleted file mode 100644 index 01fd5e8fb..000000000 Binary files a/appholder/src/main/res/drawable/img_erika_signature.jpg and /dev/null differ diff --git a/appholder/src/main/res/drawable/pager_indicator_selector.xml b/appholder/src/main/res/drawable/pager_indicator_selector.xml deleted file mode 100644 index f970bd233..000000000 --- a/appholder/src/main/res/drawable/pager_indicator_selector.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/selected_pager_dot.xml b/appholder/src/main/res/drawable/selected_pager_dot.xml deleted file mode 100644 index d2408fc64..000000000 --- a/appholder/src/main/res/drawable/selected_pager_dot.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/yellow_gradient.xml b/appholder/src/main/res/drawable/yellow_gradient.xml deleted file mode 100644 index 74d41f501..000000000 --- a/appholder/src/main/res/drawable/yellow_gradient.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/layout-land/fragment_share_document.xml b/appholder/src/main/res/layout-land/fragment_share_document.xml deleted file mode 100644 index eb330fdb5..000000000 --- a/appholder/src/main/res/layout-land/fragment_share_document.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - -