[PM-20127] Only prompt passkey user verification once (#5026)

This commit is contained in:
Patrick Honkonen
2025-04-16 15:35:25 +00:00
committed by GitHub
parent 9dd71eaea2
commit ff72efe0ed
15 changed files with 530 additions and 104 deletions
@@ -374,10 +374,8 @@ class MainViewModel @Inject constructor(
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CreateCredentialRequest.providerRequest
.biometricPromptResult
?.isSuccessful
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
fido2CredentialManager.isUserVerified =
fido2CreateCredentialRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
@@ -393,12 +391,11 @@ class MainViewModel @Inject constructor(
}
fido2AssertCredentialRequest != null -> {
// If device biometric verification was performed as part of single-tap
// authentication, set the user's verification state to the device result.
// Otherwise, retain the verification state as-is.
fido2AssertCredentialRequest.providerRequest.biometricPromptResult
?.isSuccessful
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CredentialManager.isUserVerified =
fido2AssertCredentialRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(
@@ -65,92 +65,6 @@ class Fido2CredentialManagerImpl(
}
}
private suspend fun registerFido2CredentialForUnprivilegedApp(
userId: String,
callingAppInfo: CallingAppInfo,
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val clientData = ClientData.DefaultWithExtraData(callingAppInfo.packageName)
val host = getOriginUrlFromAttestationOptionsOrNull(
requestJson = createPublicKeyCredentialRequest.requestJson,
)
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
val signatureFingerprint = callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
val sdkOrigin = Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = signatureFingerprint,
host = host,
assetLinkUrl = host,
),
)
return registerFido2CredentialInternal(
userId = userId,
sdkOrigin = sdkOrigin,
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
selectedCipherView = selectedCipherView,
clientData = clientData,
)
}
private suspend fun registerFido2CredentialForPrivilegedApp(
userId: String,
callingAppInfo: CallingAppInfo,
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val clientData = callingAppInfo
.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
val sdkOrigin = createPublicKeyCredentialRequest.origin
?.let { Origin.Web(it) }
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
return registerFido2CredentialInternal(
userId = userId,
sdkOrigin = sdkOrigin,
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
selectedCipherView = selectedCipherView,
clientData = clientData,
)
}
private suspend fun registerFido2CredentialInternal(
userId: String,
sdkOrigin: Origin,
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
clientData: ClientData,
): Fido2RegisterCredentialResult = vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = sdkOrigin,
requestJson = """{"publicKey": ${createPublicKeyCredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
// User verification is handled prior to engaging the SDK. We always respond
// `true` so that the SDK does not fail if the relying party requests UV.
isUserVerificationSupported = true,
),
fido2CredentialStore = this,
)
.map { it.toAndroidAttestationResponse() }
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = { Fido2RegisterCredentialResult.Error.InternalError },
)
override fun getPasskeyAttestationOptionsOrNull(
requestJson: String,
): PasskeyAttestationOptions? =
@@ -254,6 +168,95 @@ class Fido2CredentialManagerImpl(
?.userVerification
?: fallbackRequirement
private suspend fun registerFido2CredentialForUnprivilegedApp(
userId: String,
callingAppInfo: CallingAppInfo,
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val clientData = ClientData.DefaultWithExtraData(callingAppInfo.packageName)
val host = getOriginUrlFromAttestationOptionsOrNull(
requestJson = createPublicKeyCredentialRequest.requestJson,
)
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
val signatureFingerprint = callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
val sdkOrigin = Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = signatureFingerprint,
host = host,
assetLinkUrl = host,
),
)
return registerFido2CredentialInternal(
userId = userId,
sdkOrigin = sdkOrigin,
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
selectedCipherView = selectedCipherView,
clientData = clientData,
)
}
private suspend fun registerFido2CredentialForPrivilegedApp(
userId: String,
callingAppInfo: CallingAppInfo,
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val clientData = callingAppInfo
.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2RegisterCredentialResult.Error.InvalidAppSignature
val sdkOrigin = createPublicKeyCredentialRequest.origin
?.let { Origin.Web(it) }
?: return Fido2RegisterCredentialResult.Error.MissingHostUrl
return registerFido2CredentialInternal(
userId = userId,
sdkOrigin = sdkOrigin,
createPublicKeyCredentialRequest = createPublicKeyCredentialRequest,
selectedCipherView = selectedCipherView,
clientData = clientData,
)
}
private suspend fun registerFido2CredentialInternal(
userId: String,
sdkOrigin: Origin,
createPublicKeyCredentialRequest: CreatePublicKeyCredentialRequest,
selectedCipherView: CipherView,
clientData: ClientData,
): Fido2RegisterCredentialResult = vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = sdkOrigin,
requestJson = """{"publicKey": ${createPublicKeyCredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
// User verification is handled prior to engaging the SDK. We always respond
// `true` so that the SDK does not fail if the relying party requests UV.
isUserVerificationSupported = true,
),
fido2CredentialStore = this,
)
.map { it.toAndroidAttestationResponse() }
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = {
Timber.e(it, "Failed to register FIDO2 credential.")
Fido2RegisterCredentialResult.Error.InternalError
},
)
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
getPasskeyAssertionOptionsOrNull(requestJson)
?.relyingPartyId
@@ -14,12 +14,15 @@ import kotlinx.parcelize.Parcelize
* credential manager framework.
*
* @property userId The ID of the user creating the passkey.
* @property isUserPreVerified Whether the user has already been verified by the OS biometric
* prompt.
* @property requestData Provider request data in the form of a [Bundle].
*/
@Parcelize
data class Fido2CreateCredentialRequest(
val userId: String,
val requestData: Bundle,
val isUserPreVerified: Boolean,
) : Parcelable {
/**
@@ -11,16 +11,19 @@ import kotlinx.parcelize.Parcelize
/**
* Models a FIDO 2 credential authentication request parsed from the launching intent.
*
* @param userId ID of the user requesting credential authentication.
* @param cipherId ID of the cipher to be authenticated against.
* @param credentialId ID of the credential to authenticate.
* @param requestData Provider request data in the form of a [Bundle].
* @property userId ID of the user requesting credential authentication.
* @property cipherId ID of the cipher to be authenticated against.
* @property credentialId ID of the credential to authenticate.
* @property isUserPreVerified Whether the user has already been verified by the OS biometric
* prompt.
* @property requestData Provider request data in the form of a [Bundle].
*/
@Parcelize
data class Fido2CredentialAssertionRequest(
val userId: String,
val cipherId: String,
val credentialId: String,
val isUserPreVerified: Boolean,
private val requestData: Bundle,
) : Parcelable {
@@ -27,8 +27,16 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
val userId = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
// Extract the OS biometric prompt result from the request data because it is not included in
// the bundle returned by `ProviderGetCredentialRequest.asBundle()`.
val isUserPreVerified = systemRequest
.biometricPromptResult
?.isSuccessful
?: false
return Fido2CreateCredentialRequest(
userId = userId,
isUserPreVerified = isUserPreVerified,
requestData = ProviderCreateCredentialRequest.asBundle(systemRequest),
)
}
@@ -53,10 +61,18 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
// Extract the OS biometric prompt result from the request data because it is not included in
// the bundle returned by `ProviderGetCredentialRequest.asBundle()`.
val isUserPreVerified = systemRequest
.biometricPromptResult
?.isSuccessful
?: false
return Fido2CredentialAssertionRequest(
userId = userId,
cipherId = cipherId,
credentialId = credentialId,
isUserPreVerified = isUserPreVerified,
requestData = ProviderGetCredentialRequest.asBundle(systemRequest),
)
}