From 0a450ed40a1c09541d7b5a45fd10cc9ec5a55fe0 Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Sun, 2 Aug 2026 02:02:28 +0200 Subject: [PATCH] Remove provenance metadata fields from passkey storage Remove createdByCallerType, createdByPackage, createdByCertificateDigest, and verifiedOrigin fields from PasskeyCredential and StoredCredential. These fields stored information about which app/browser created the passkey (e.g., org.mozilla.fenix), but this metadata is not needed for WebAuthn ceremonies and adds unnecessary complexity to the storage format. Old credentials with these fields will continue to work - the extra CBOR keys are safely ignored during deserialization. Deleted StoredCredentialBindingMetadataTest.kt as it tested the removed functionality. Fixes compatibility issues with tools like soft-fido2 that expect strict WebAuthn-compliant CBOR encoding. --- .../passkeys/AppPasskeyProviderActivity.kt | 21 +-- .../passkeys/model/PasskeyCredential.kt | 13 -- .../passkeys/model/StoredCredential.kt | 53 ------ .../StoredCredentialBindingMetadataTest.kt | 164 ------------------ 4 files changed, 7 insertions(+), 244 deletions(-) delete mode 100644 passkeys/core/src/test/kotlin/app/passwordstore/passkeys/model/StoredCredentialBindingMetadataTest.kt diff --git a/app/src/main/java/app/passwordstore/passkeys/AppPasskeyProviderActivity.kt b/app/src/main/java/app/passwordstore/passkeys/AppPasskeyProviderActivity.kt index e74237742..06b5c97d0 100644 --- a/app/src/main/java/app/passwordstore/passkeys/AppPasskeyProviderActivity.kt +++ b/app/src/main/java/app/passwordstore/passkeys/AppPasskeyProviderActivity.kt @@ -573,23 +573,16 @@ class AppPasskeyProviderActivity : BaseGitActivity() { try { val publicCredential = createdCredential.credential - val credentialWithBinding = - publicCredential.copy( - createdByCallerType = verifiedContext.callerType, - createdByPackage = verifiedContext.callingPackage, - createdByCertificateDigest = verifiedContext.signingCertificateDigests.firstOrNull(), - verifiedOrigin = verifiedContext.origin, - ) val responseJson = PasskeyProviderUtils.buildAttestationResponse( - credentialWithBinding, + publicCredential, createRequest.requestJson, verifiedContext, ) val saveResult = createdCredential.usePrivateKeySuspend { privateKey -> - passkeyStorage.saveCredential(credentialWithBinding, privateKey) + passkeyStorage.saveCredential(publicCredential, privateKey) } if (saveResult.isErr) { val error: Throwable? = @@ -611,12 +604,12 @@ class AppPasskeyProviderActivity : BaseGitActivity() { passkeyRepositoryState.onCredentialSaved() generationProvider.bumpWorktreeGeneration() metadataIndex.put( - credentialWithBinding.credentialId, + publicCredential.credentialId, MetadataEntry( - userName = credentialWithBinding.user.name, - userDisplayName = credentialWithBinding.user.displayName, - rpId = credentialWithBinding.rpId, - createdAt = credentialWithBinding.createdAt.toEpochMilliseconds(), + userName = publicCredential.user.name, + userDisplayName = publicCredential.user.displayName, + rpId = publicCredential.rpId, + createdAt = publicCredential.createdAt.toEpochMilliseconds(), ), ) diff --git a/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/PasskeyCredential.kt b/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/PasskeyCredential.kt index 8a0a11f9b..ac0dbf7ba 100644 --- a/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/PasskeyCredential.kt +++ b/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/PasskeyCredential.kt @@ -7,7 +7,6 @@ package app.passwordstore.passkeys.model -import app.passwordstore.passkeys.crypto.CallerType import kotlin.time.Instant import kotlinx.serialization.Serializable @@ -23,10 +22,6 @@ public data class PasskeyCredential( public val uvInitialized: Boolean = true, public val backupEligible: Boolean = true, public val backupState: Boolean = false, - public val createdByCallerType: CallerType? = null, - public val createdByPackage: String? = null, - public val createdByCertificateDigest: String? = null, - public val verifiedOrigin: String? = null, ) { init { @@ -51,10 +46,6 @@ public data class PasskeyCredential( if (uvInitialized != other.uvInitialized) return false if (backupEligible != other.backupEligible) return false if (backupState != other.backupState) return false - if (createdByCallerType != other.createdByCallerType) return false - if (createdByPackage != other.createdByPackage) return false - if (createdByCertificateDigest != other.createdByCertificateDigest) return false - if (verifiedOrigin != other.verifiedOrigin) return false return true } @@ -69,10 +60,6 @@ public data class PasskeyCredential( result = 31 * result + uvInitialized.hashCode() result = 31 * result + backupEligible.hashCode() result = 31 * result + backupState.hashCode() - result = 31 * result + (createdByCallerType?.hashCode() ?: 0) - result = 31 * result + (createdByPackage?.hashCode() ?: 0) - result = 31 * result + (createdByCertificateDigest?.hashCode() ?: 0) - result = 31 * result + (verifiedOrigin?.hashCode() ?: 0) return result } diff --git a/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/StoredCredential.kt b/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/StoredCredential.kt index 76d5a05e3..1e897c7d7 100644 --- a/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/StoredCredential.kt +++ b/passkeys/core/src/main/kotlin/app/passwordstore/passkeys/model/StoredCredential.kt @@ -11,7 +11,6 @@ import app.passwordstore.passkeys.cbor.Cbor import app.passwordstore.passkeys.cbor.CborMap import app.passwordstore.passkeys.cbor.CborValue import app.passwordstore.passkeys.cbor.toCborIntegerArray -import app.passwordstore.passkeys.crypto.CallerType import java.math.BigInteger import kotlin.time.Instant import logcat.LogPriority @@ -31,10 +30,6 @@ public data class StoredCredential( val extensions: Extensions = Extensions(), val backupEligible: Boolean = true, val backupState: Boolean = false, - val createdByCallerType: CallerType? = null, - val createdByPackage: String? = null, - val createdByCertificateDigest: String? = null, - val verifiedOrigin: String? = null, ) : AutoCloseable { @Volatile private var wiped = false @@ -70,18 +65,6 @@ public data class StoredCredential( map["extensions"] = CborValue.Map(extensions.toCborMap()) map["backup_eligible"] = if (backupEligible) CborValue.True else CborValue.False map["backup_state"] = if (backupState) CborValue.True else CborValue.False - createdByCallerType?.let { - map["created_by_caller_type"] = CborValue.TextString(it.name) - } ?: run { map["created_by_caller_type"] = CborValue.Null } - createdByPackage?.let { - map["created_by_package"] = CborValue.TextString(it) - } ?: run { map["created_by_package"] = CborValue.Null } - createdByCertificateDigest?.let { - map["created_by_cert_digest"] = CborValue.TextString(it) - } ?: run { map["created_by_cert_digest"] = CborValue.Null } - verifiedOrigin?.let { - map["verified_origin"] = CborValue.TextString(it) - } ?: run { map["verified_origin"] = CborValue.Null } return Cbor.fromMap(CborMap.from(map)).toBytes() } @@ -97,10 +80,6 @@ public data class StoredCredential( uvInitialized = true, backupEligible = backupEligible, backupState = backupState, - createdByCallerType = createdByCallerType, - createdByPackage = createdByPackage, - createdByCertificateDigest = createdByCertificateDigest, - verifiedOrigin = verifiedOrigin, ) } @@ -126,10 +105,6 @@ public data class StoredCredential( if (extensions != other.extensions) return false if (backupEligible != other.backupEligible) return false if (backupState != other.backupState) return false - if (createdByCallerType != other.createdByCallerType) return false - if (createdByPackage != other.createdByPackage) return false - if (createdByCertificateDigest != other.createdByCertificateDigest) return false - if (verifiedOrigin != other.verifiedOrigin) return false return true } @@ -146,10 +121,6 @@ public data class StoredCredential( result = 31 * result + extensions.hashCode() result = 31 * result + backupEligible.hashCode() result = 31 * result + backupState.hashCode() - result = 31 * result + (createdByCallerType?.hashCode() ?: 0) - result = 31 * result + (createdByPackage?.hashCode() ?: 0) - result = 31 * result + (createdByCertificateDigest?.hashCode() ?: 0) - result = 31 * result + (verifiedOrigin?.hashCode() ?: 0) return result } @@ -211,22 +182,6 @@ public data class StoredCredential( val extensionsMap = map.getMap("extensions") val backupEligible = map.getBoolean("backup_eligible") ?: true val backupState = map.getBoolean("backup_state") ?: false - val createdByCallerType = - if (map.isNull("created_by_caller_type")) null - else - map.getString("created_by_caller_type")?.let { - try { - CallerType.valueOf(it) - } catch (_: Exception) { - null - } - } - val createdByPackage = - if (map.isNull("created_by_package")) null else map.getString("created_by_package") - val createdByCertificateDigest = - if (map.isNull("created_by_cert_digest")) null else map.getString("created_by_cert_digest") - val verifiedOrigin = - if (map.isNull("verified_origin")) null else map.getString("verified_origin") return StoredCredential( id = id, @@ -241,10 +196,6 @@ public data class StoredCredential( extensions = extensionsMap?.let { Extensions.fromCborMap(it) } ?: Extensions(), backupEligible = backupEligible, backupState = backupState, - createdByCallerType = createdByCallerType, - createdByPackage = createdByPackage, - createdByCertificateDigest = createdByCertificateDigest, - verifiedOrigin = verifiedOrigin, ) } @@ -301,10 +252,6 @@ public data class StoredCredential( extensions = Extensions(), backupEligible = credential.backupEligible, backupState = credential.backupState, - createdByCallerType = credential.createdByCallerType, - createdByPackage = credential.createdByPackage, - createdByCertificateDigest = credential.createdByCertificateDigest, - verifiedOrigin = credential.verifiedOrigin, ) } } diff --git a/passkeys/core/src/test/kotlin/app/passwordstore/passkeys/model/StoredCredentialBindingMetadataTest.kt b/passkeys/core/src/test/kotlin/app/passwordstore/passkeys/model/StoredCredentialBindingMetadataTest.kt deleted file mode 100644 index 2756e63cb..000000000 --- a/passkeys/core/src/test/kotlin/app/passwordstore/passkeys/model/StoredCredentialBindingMetadataTest.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -@file:OptIn(kotlin.time.ExperimentalTime::class) - -package app.passwordstore.passkeys.model - -import app.passwordstore.passkeys.crypto.CallerType -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test - -class StoredCredentialBindingMetadataTest { - - private fun baseCredential(): StoredCredential = - StoredCredential( - id = ByteArray(32) { it.toByte() }, - rp = RelyingParty(id = "example.com", name = "Example"), - user = User(id = "user1".toByteArray(), name = "testuser", displayName = "Test User"), - signCount = 0u, - alg = StoredCredential.ALG_ES256, - privateKey = ByteArray(32) { (it + 1).toByte() }, - publicKey = ByteArray(65).also { it[0] = 0x04 }, - created = 1700000000L, - ) - - @Test - fun `binding metadata round-trips through CBOR`() { - val original = - baseCredential() - .copy( - createdByCallerType = CallerType.NATIVE_APP, - createdByPackage = "com.example.app", - createdByCertificateDigest = "abc123digest", - verifiedOrigin = "android:apk-key-hash:xyz", - ) - - val cbor = original.toCbor() - val restored = StoredCredential.fromCbor(cbor) - - assertEquals(CallerType.NATIVE_APP, restored.createdByCallerType) - assertEquals("com.example.app", restored.createdByPackage) - assertEquals("abc123digest", restored.createdByCertificateDigest) - assertEquals("android:apk-key-hash:xyz", restored.verifiedOrigin) - } - - @Test - fun `null binding metadata round-trips through CBOR`() { - val original = baseCredential() - val cbor = original.toCbor() - val restored = StoredCredential.fromCbor(cbor) - - assertNull(restored.createdByCallerType) - assertNull(restored.createdByPackage) - assertNull(restored.createdByCertificateDigest) - assertNull(restored.verifiedOrigin) - } - - @Test - fun `browser caller type round-trips`() { - val original = - baseCredential() - .copy( - createdByCallerType = CallerType.PRIVILEGED_BROWSER, - createdByPackage = "com.android.chrome", - createdByCertificateDigest = "browserdigest", - verifiedOrigin = "https://example.com", - ) - - val cbor = original.toCbor() - val restored = StoredCredential.fromCbor(cbor) - - assertEquals(CallerType.PRIVILEGED_BROWSER, restored.createdByCallerType) - assertEquals("com.android.chrome", restored.createdByPackage) - assertEquals("https://example.com", restored.verifiedOrigin) - } - - @Test - fun `toPasskeyCredential preserves binding metadata`() { - val stored = - baseCredential() - .copy( - createdByCallerType = CallerType.NATIVE_APP, - createdByPackage = "com.example", - createdByCertificateDigest = "digest", - verifiedOrigin = "android:apk-key-hash:abc", - ) - - val passkey = stored.toPasskeyCredential() - - assertEquals(CallerType.NATIVE_APP, passkey.createdByCallerType) - assertEquals("com.example", passkey.createdByPackage) - assertEquals("digest", passkey.createdByCertificateDigest) - assertEquals("android:apk-key-hash:abc", passkey.verifiedOrigin) - } - - @Test - fun `fromPasskeyCredential preserves binding metadata`() { - val privateKey = ByteArray(32) - val passkey = - app.passwordstore.passkeys.model.PasskeyCredential( - credentialId = ByteArray(32), - publicKey = ByteArray(65).also { it[0] = 0x04 }, - rpId = "example.com", - user = FidoUser(id = ByteArray(4), name = "user", displayName = "User"), - createdAt = kotlin.time.Instant.fromEpochSeconds(1700000000L), - createdByCallerType = CallerType.PRIVILEGED_BROWSER, - createdByPackage = "com.brave.browser", - createdByCertificateDigest = "bravedigest", - verifiedOrigin = "https://example.com", - ) - - val stored = StoredCredential.fromPasskeyCredential(passkey, privateKey) - - assertEquals(CallerType.PRIVILEGED_BROWSER, stored.createdByCallerType) - assertEquals("com.brave.browser", stored.createdByPackage) - assertEquals("bravedigest", stored.createdByCertificateDigest) - assertEquals("https://example.com", stored.verifiedOrigin) - } - - @Test - fun `fromPasskeyCredential owns arrays it wipes`() { - val credentialId = ByteArray(32) { (it + 1).toByte() } - val publicKey = ByteArray(65) { if (it == 0) 0x04.toByte() else it.toByte() } - val userId = ByteArray(16) { (it + 2).toByte() } - val privateKey = ByteArray(32) { (it + 3).toByte() } - val passkey = - PasskeyCredential( - credentialId = credentialId, - publicKey = publicKey, - rpId = "example.com", - user = FidoUser(id = userId, name = "user", displayName = "User"), - createdAt = kotlin.time.Instant.fromEpochSeconds(1700000000L), - ) - val expectedCredentialId = credentialId.copyOf() - val expectedPublicKey = publicKey.copyOf() - val expectedUserId = userId.copyOf() - val expectedPrivateKey = privateKey.copyOf() - - StoredCredential.fromPasskeyCredential(passkey, privateKey).close() - - assertArrayEquals(expectedCredentialId, credentialId) - assertArrayEquals(expectedPublicKey, publicKey) - assertArrayEquals(expectedUserId, userId) - assertArrayEquals(expectedPrivateKey, privateKey) - } - - @Test - fun `old credential without binding fields can be parsed`() { - val original = baseCredential() - val cbor = original.toCbor() - val restored = StoredCredential.fromCbor(cbor) - - assertNull(restored.createdByCallerType) - assertNull(restored.createdByPackage) - assertNull(restored.createdByCertificateDigest) - assertNull(restored.verifiedOrigin) - assertEquals("example.com", restored.rp.id) - assertArrayEquals(original.id, restored.id) - } -}