mirror of
https://github.com/bitwarden/android.git
synced 2026-08-01 03:05:47 -05:00
[PM-20127] Only prompt passkey user verification once (#5026)
This commit is contained in:
@@ -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(
|
||||
|
||||
+89
-86
@@ -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
|
||||
|
||||
+3
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
+7
-4
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user