-
Notifications
You must be signed in to change notification settings - Fork 903
[PM-24971] Sanitize passkey attestation options from AliExpress #6106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
33b555b
b3a5b2c
6544a46
1f03aad
52a0b55
50cfa57
dd92be6
4c5023d
7fa03d6
df37505
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest | |
| import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions | ||
| import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions | ||
| import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement | ||
| import com.x8bit.bitwarden.data.credentials.sanitizer.PasskeyAttestationOptionsSanitizer | ||
| import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager | ||
| import com.x8bit.bitwarden.data.platform.util.getAppOrigin | ||
| import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint | ||
|
|
@@ -60,6 +61,7 @@ class BitwardenCredentialManagerImpl( | |
| private val json: Json, | ||
| private val vaultRepository: VaultRepository, | ||
| private val cipherMatchingManager: CipherMatchingManager, | ||
| private val passkeyAttestationOptionsSanitizer: PasskeyAttestationOptionsSanitizer, | ||
| dispatcherManager: DispatcherManager, | ||
| ) : BitwardenCredentialManager, | ||
| Fido2CredentialStore by fido2CredentialStore { | ||
|
|
@@ -359,31 +361,46 @@ class BitwardenCredentialManagerImpl( | |
| selectedCipherView: CipherView, | ||
| clientData: ClientData, | ||
| callingPackageName: String, | ||
| ): 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(callingPackageName = callingPackageName) | ||
| } | ||
| .mapCatching { json.encodeToString(it) } | ||
| .fold( | ||
| onSuccess = { Fido2RegisterCredentialResult.Success(it) }, | ||
| onFailure = { | ||
| Timber.e(it, "Failed to register FIDO2 credential.") | ||
| Fido2RegisterCredentialResult.Error.InternalError | ||
| }, | ||
| ) | ||
| ): Fido2RegisterCredentialResult { | ||
| val requestJson = | ||
| getPasskeyAttestationOptionsOrNull(createPublicKeyCredentialRequest.requestJson) | ||
| ?.let { passkeyAttestationOptionsSanitizer.sanitize(options = it) } | ||
| ?.runCatching { json.encodeToString(this) } | ||
| ?.fold( | ||
| onSuccess = { it }, | ||
| onFailure = { | ||
| Timber.e(it, "Failed to sanitize passkey attestation options.") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ |
||
| null | ||
| }, | ||
| ) | ||
| ?: return Fido2RegisterCredentialResult.Error.InternalError | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, if sanitization fails, we only log the error and return
Example: onFailure = {
Timber.e(it, "Failed to sanitize passkey attestation options for RP: ${options.relyingParty.id}")
null
},This would help distinguish sanitization failures from other internal errors when investigating issues. |
||
|
|
||
| return vaultSdkSource | ||
| .registerFido2Credential( | ||
| request = RegisterFido2CredentialRequest( | ||
| userId = userId, | ||
| origin = sdkOrigin, | ||
| requestJson = """{"publicKey": $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(callingPackageName = callingPackageName) | ||
| } | ||
| .mapCatching { json.encodeToString(it) } | ||
| .fold( | ||
| onSuccess = { Fido2RegisterCredentialResult.Success(it) }, | ||
| onFailure = { | ||
| Timber.e(it, "Failed to register FIDO2 credential.") | ||
| Fido2RegisterCredentialResult.Error.InternalError | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| private fun List<BeginGetPasswordOption>.toPasswordCredentialEntries( | ||
| userId: String, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.x8bit.bitwarden.data.credentials.sanitizer | ||
|
|
||
| import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions | ||
|
|
||
| /** | ||
| * Defines a contract for sanitizing [PasskeyAttestationOptions] received from applications. | ||
| * | ||
| * Sanitization applies workarounds for known issues with specific applications' | ||
SaintPatrck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * passkey implementations, ensuring the options are in the correct format before | ||
| * being used to create a passkey credential. | ||
| */ | ||
| interface PasskeyAttestationOptionsSanitizer { | ||
|
|
||
| /** | ||
| * Sanitizes the given [PasskeyAttestationOptions] in preparation for use in the | ||
| * passkey creation process. | ||
| * | ||
| * @param options The [PasskeyAttestationOptions] to sanitize. | ||
| * @return A new, sanitized instance of [PasskeyAttestationOptions]. | ||
| */ | ||
| fun sanitize(options: PasskeyAttestationOptions): PasskeyAttestationOptions | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.x8bit.bitwarden.data.credentials.sanitizer | ||
|
|
||
| import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions | ||
|
|
||
| /** | ||
| * Default implementation of [PasskeyAttestationOptionsSanitizer]. | ||
| */ | ||
| object PasskeyAttestationOptionsSanitizerImpl : PasskeyAttestationOptionsSanitizer { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ |
||
| override fun sanitize(options: PasskeyAttestationOptions): PasskeyAttestationOptions { | ||
| // The AliExpress Android app (com.alibaba.aliexpresshd) incorrectly appends a newline | ||
| // to the user.id field when creating a passkey. This causes the operation to fail | ||
| // downstream. As a workaround, we detect this specific scenario, trim the newline, and | ||
| // re-serialize the JSON request. | ||
| return if (options.relyingParty.id == ALIEXPRESS_RP_ID && | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any downside to doing it on all relaying parties?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Performance wise, the difference is negligible. I chose this route more to retain data integrity for other RP's, and ease of removal when we no longer need the workaround. I plan to open a PR that performs validation and sanitizes request properties in the SDK, but I don't want that to hold-up fixing this issue.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes snese |
||
| options.user.id.endsWith("\n") | ||
| ) { | ||
| options.copy( | ||
| user = options.user.copy(id = options.user.id.trimEnd('\n')), | ||
SaintPatrck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| } else { | ||
| options | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private const val ALIEXPRESS_RP_ID = "m.aliexpress.com" | ||
Uh oh!
There was an error while loading. Please reload this page.