Skip to content

Commit

Permalink
Refactor consent prompt and move it to identity-appsupport.
Browse files Browse the repository at this point in the history
- Add support for `intentToRetain` and add new sample requests
  to identity-doctypes library.
- Make `TrustPoint` available in multiplatform. This includes switching
  to using X509Cert instead of X509Certificate (which is Java-only).
- Update to match design and UX in the functional specification document.
- Rename to `ConsentModalBottomSheet` and for easier embedding take
  a `SheetState`.
- Introduce `ConsentDocument` and `ConsentRelyingParty` to capture
  information to display in the sheet.
- Fix height so it's not constant-height but dynamically adjusts
  according to how much content there is
- Instead of disabling the "Share" button and displaying "Scroll down",
  turn the "Share" button into a "More" button which will scroll down.
  This matches the behavior in other wallets
- Add support in samples/testapp for this for all the use-cases.
- Use Material You in samples/testapp
- Rework navigation in samples/testapp
- Update some dependencies.

Test: Manually tested in samples/testapp on Android and iOS.
Signed-off-by: David Zeuthen <[email protected]>
  • Loading branch information
davidz25 committed Oct 9, 2024
1 parent 67af575 commit 3b77d5b
Show file tree
Hide file tree
Showing 51 changed files with 1,372 additions and 729 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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
Expand Down Expand Up @@ -64,10 +65,10 @@ class HolderApp: Application() {
certificateStorageEngineInstance = certificateStorageEngine
certificateStorageEngineInstance.enumerate().forEach {
val certificate = parseCertificate(certificateStorageEngineInstance.get(it)!!)
trustManagerInstance.addTrustPoint(TrustPoint(certificate))
trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded)))
}
KeysAndCertificates.getTrustedReaderCertificates(this).forEach {
trustManagerInstance.addTrustPoint(TrustPoint(it))
trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(it.encoded)))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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
Expand Down Expand Up @@ -77,7 +78,7 @@ class CaCertificatesFragment : Fragment() {
this.requireContext().contentResolver.openInputStream(uri).use { inputStream ->
if (inputStream != null) {
val certificate = parseCertificate(inputStream.readBytes())
HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(certificate))
HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded)))
HolderApp.certificateStorageEngineInstance.put(
certificate.getSubjectKeyIdentifier(),
certificate.encoded
Expand All @@ -102,7 +103,7 @@ class CaCertificatesFragment : Fragment() {
}
val text = clipboard.primaryClip?.getItemAt(0)?.text!!
val certificate = parseCertificate(text.toString().toByteArray())
HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(certificate))
HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded)))
HolderApp.certificateStorageEngineInstance.put(
certificate.getSubjectKeyIdentifier(),
certificate.encoded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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
Expand Down Expand Up @@ -31,7 +32,9 @@ class CaCertificatesViewModel() : ViewModel() {
fun deleteCertificate() {
_currentCertificateItem.value?.trustPoint?.let {
HolderApp.trustManagerInstance.removeTrustPoint(it)
HolderApp.certificateStorageEngineInstance.delete(it.certificate.getSubjectKeyIdentifier())
HolderApp.certificateStorageEngineInstance.delete(
it.certificate.javaX509Certificate.getSubjectKeyIdentifier()
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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
Expand All @@ -10,16 +11,16 @@ import java.lang.StringBuilder
import java.security.MessageDigest

fun TrustPoint.toCertificateItem(docTypes: List<String> = emptyList()): CertificateItem {
val subject = this.certificate.subjectX500Principal
val issuer = this.certificate.issuerX500Principal
val subject = this.certificate.javaX509Certificate.subjectX500Principal
val issuer = this.certificate.javaX509Certificate.issuerX500Principal
val sha255Fingerprint = hexWithSpaces(
MessageDigest.getInstance("SHA-256").digest(
this.certificate.encoded
this.certificate.encodedCertificate
)
)
val sha1Fingerprint = hexWithSpaces(
MessageDigest.getInstance("SHA-1").digest(
this.certificate.encoded
this.certificate.encodedCertificate
)
)
val defaultValue = "<Not part of certificate>"
Expand All @@ -32,12 +33,14 @@ fun TrustPoint.toCertificateItem(docTypes: List<String> = emptyList()): Certific
commonNameIssuer = issuer.getCommonName(defaultValue),
organisationIssuer = issuer.getOrganisation(defaultValue),
organisationalUnitIssuer = issuer.organisationalUnit(defaultValue),
notBefore = this.certificate.notBefore,
notAfter = this.certificate.notAfter,
notBefore = this.certificate.javaX509Certificate.notBefore,
notAfter = this.certificate.javaX509Certificate.notAfter,
sha255Fingerprint = sha255Fingerprint,
sha1Fingerprint = sha1Fingerprint,
docTypes = docTypes,
supportsDelete = HolderApp.certificateStorageEngineInstance.get(this.certificate.getSubjectKeyIdentifier()) != null ,
supportsDelete = HolderApp.certificateStorageEngineInstance.get(
this.certificate.javaX509Certificate.getSubjectKeyIdentifier()
) != null,
trustPoint = this
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ class VerifierApp : Application() {
certificateStorageEngineInstance = certificateStorageEngine
certificateStorageEngineInstance.enumerate().forEach {
val certificate = parseCertificate(certificateStorageEngineInstance.get(it)!!)
trustManagerInstance.addTrustPoint(TrustPoint(certificate))
trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded)))
}
KeysAndCertificates.getTrustedIssuerCertificates(this).forEach {
trustManagerInstance.addTrustPoint(TrustPoint(it))
trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(it.encoded)))
}
val signedVical = SignedVical.parse(
resources.openRawResource(R.raw.austroad_test_event_vical_20241002).readBytes()
Expand All @@ -71,7 +71,7 @@ class VerifierApp : Application() {
val cert = X509Cert(certInfo.certificate)
trustManagerInstance.addTrustPoint(
TrustPoint(
cert.javaX509Certificate,
cert,
null,
null
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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.mdl.appreader.VerifierApp
import com.android.mdl.appreader.theme.ReaderAppTheme
Expand Down Expand Up @@ -77,7 +78,7 @@ class CaCertificatesFragment : Fragment() {
this.requireContext().contentResolver.openInputStream(uri).use { inputStream ->
if (inputStream != null) {
val certificate = parseCertificate(inputStream.readBytes())
VerifierApp.trustManagerInstance.addTrustPoint(TrustPoint(certificate))
VerifierApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded)))
VerifierApp.certificateStorageEngineInstance.put(
certificate.getSubjectKeyIdentifier(),
certificate.encoded
Expand All @@ -102,7 +103,7 @@ class CaCertificatesFragment : Fragment() {
}
val text = clipboard.primaryClip?.getItemAt(0)?.text!!
val certificate = parseCertificate(text.toString().toByteArray())
VerifierApp.trustManagerInstance.addTrustPoint(TrustPoint(certificate))
VerifierApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded)))
VerifierApp.certificateStorageEngineInstance.put(
certificate.getSubjectKeyIdentifier(),
certificate.encoded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.mdl.appreader.VerifierApp
import com.android.mdl.appreader.trustmanagement.getSubjectKeyIdentifier
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -31,7 +32,7 @@ class CaCertificatesViewModel() : ViewModel() {
fun deleteCertificate() {
_currentCertificateItem.value?.trustPoint?.let {
VerifierApp.trustManagerInstance.removeTrustPoint(it)
VerifierApp.certificateStorageEngineInstance.delete(it.certificate.getSubjectKeyIdentifier())
VerifierApp.certificateStorageEngineInstance.delete(it.certificate.javaX509Certificate.getSubjectKeyIdentifier())
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.android.mdl.appreader.settings

import com.android.identity.crypto.javaX509Certificate
import com.android.identity.trustmanagement.TrustPoint
import com.android.mdl.appreader.VerifierApp
import com.android.mdl.appreader.trustmanagement.getCommonName
Expand All @@ -10,16 +11,16 @@ import java.lang.StringBuilder
import java.security.MessageDigest

fun TrustPoint.toCertificateItem(docTypes: List<String> = emptyList()): CertificateItem {
val subject = this.certificate.subjectX500Principal
val issuer = this.certificate.issuerX500Principal
val subject = this.certificate.javaX509Certificate.subjectX500Principal
val issuer = this.certificate.javaX509Certificate.issuerX500Principal
val sha255Fingerprint = hexWithSpaces(
MessageDigest.getInstance("SHA-256").digest(
this.certificate.encoded
this.certificate.encodedCertificate
)
)
val sha1Fingerprint = hexWithSpaces(
MessageDigest.getInstance("SHA-1").digest(
this.certificate.encoded
this.certificate.encodedCertificate
)
)
val defaultValue = "<Not part of certificate>"
Expand All @@ -32,12 +33,14 @@ fun TrustPoint.toCertificateItem(docTypes: List<String> = emptyList()): Certific
commonNameIssuer = issuer.getCommonName(defaultValue),
organisationIssuer = issuer.getOrganisation(defaultValue),
organisationalUnitIssuer = issuer.organisationalUnit(defaultValue),
notBefore = this.certificate.notBefore,
notAfter = this.certificate.notAfter,
notBefore = this.certificate.javaX509Certificate.notBefore,
notAfter = this.certificate.javaX509Certificate.notAfter,
sha255Fingerprint = sha255Fingerprint,
sha1Fingerprint = sha1Fingerprint,
docTypes = docTypes,
supportsDelete = VerifierApp.certificateStorageEngineInstance.get(this.certificate.getSubjectKeyIdentifier()) != null ,
supportsDelete = VerifierApp.certificateStorageEngineInstance.get(
this.certificate.javaX509Certificate.getSubjectKeyIdentifier()
) != null,
trustPoint = this
)
}
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ androidx-core-ktx = "1.13.1"
androidx-espresso-core = "3.5.1"
androidx-material = "1.12.0"
androidx-test-junit = "1.1.5"
compose-plugin = "1.6.10"
compose-plugin = "1.7.0-rc01"
faceDetection = "16.1.6"
junit = "4.13.2"
kotlin = "2.0.0"
Expand All @@ -29,7 +29,7 @@ scuba = "0.0.26"
jmrtd = "0.7.42"
mlkit = "16.0.0"
camera = "1.3.3"
compose-material3 = "1.2.1"
compose-material3 = "1.3.0"
compose-material-icons-extended = "1.6.7"
androidx-navigation = "2.7.7"
code-scanner = "2.3.2"
Expand Down
41 changes: 41 additions & 0 deletions identity-appsupport/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}
Expand All @@ -9,6 +13,13 @@ kotlin {

jvm()

androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

listOf(
iosX64(),
iosArm64(),
Expand Down Expand Up @@ -41,9 +52,39 @@ kotlin {
implementation(libs.jetbrains.navigation.runtime)

implementation(project(":identity"))
implementation(project(":identity-mdoc"))
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.io.core)
}
}

val jvmMain by getting {
dependencies {
implementation(libs.bouncy.castle.bcprov)
implementation(libs.bouncy.castle.bcpkix)
implementation(libs.tink)
}
}

val androidMain by getting {
dependencies {
implementation(libs.bouncy.castle.bcprov)
implementation(libs.bouncy.castle.bcpkix)
implementation(libs.tink)
}
}
}
}

android {
namespace = "com.android.identity.appsupport"
compileSdk = libs.versions.android.compileSdk.get().toInt()

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
debugImplementation(compose.uiTooling)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.android.identity.appsupport.ui

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

@Composable
actual fun AppTheme(content: @Composable () -> Unit) {
val darkScheme = isSystemInDarkTheme()
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val context = LocalContext.current
if (darkScheme) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
} else {
if (darkScheme) {
darkColorScheme()
} else {
lightColorScheme()
}
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,22 @@
<!-- PassphraseEntryField -->
<string name="passphrase_entry_field_pin_is_weak">PIN is weak, please choose another</string>
<string name="passphrase_entry_field_passphrase_is_weak">Passphrase is weak, please choose another</string>

<!-- ConsentModalBottomSheet -->
<string name="consent_modal_bottom_sheet_button_cancel">Cancel</string>
<string name="consent_modal_bottom_sheet_button_more">More</string>
<string name="consent_modal_bottom_sheet_button_share">Share</string>
<string name="consent_modal_bottom_sheet_headline_share_with_known_requester">Share with %1$s</string>
<string name="consent_modal_bottom_sheet_headline_share_with_unknown_requester">Share with Unknown requester</string>
<string name="consent_modal_bottom_sheet_share_with_known_requester">The following information will be shared with %1$s:</string>
<string name="consent_modal_bottom_sheet_share_with_unknown_requester">The following information will be shared:</string>
<string name="consent_modal_bottom_sheet_share_and_stored_by_known_requester">The following information will be shared with and stored by %1$s:</string>
<string name="consent_modal_bottom_sheet_share_and_stored_by_unknown_requester">The following information will be shared with and stored by the requester:</string>
<string name="consent_modal_bottom_sheet_wallet_privacy_policy">The OWF Identity Credential Website describes how your data is being handled</string>
<string name="consent_modal_bottom_sheet_warning_verifier_not_in_trust_list">The verifier requesting this data is not in a trust list. Make sure you are comfortable sharing this data with them.</string>
<string name="consent_modal_bottom_sheet_card_art_description">Card Art</string>
<string name="consent_modal_bottom_sheet_verifier_icon_description">Verifier Icon</string>
<string name="consent_modal_bottom_sheet_data_element_icon_description">Data Element Icon</string>
<string name="consent_modal_bottom_sheet_warning_icon_description">Warning Icon</string>

</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.android.identity.appsupport.ui

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable

@Composable
expect fun AppTheme(content: @Composable () -> Unit)

@Composable
fun AppThemeDefault(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(),
content = content
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.android.identity.appsupport.ui.consent

/**
* Details with the document that is being presented in the consent dialog.
*
* @property name the name of the document e.g. "Erika's Driving License"
* @property description the description e.g. "Driving License" or "Government-Issued ID"
* @property cardArt the card art for the document
*/
data class ConsentDocument(
val name: String,
val description: String,
val cardArt: ByteArray
)
Loading

0 comments on commit 3b77d5b

Please sign in to comment.