Skip to content

[PM-11886] Update handling of unprivileged apps and improve error messaging #4694

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

Merged
merged 13 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.bitwarden.fido.Origin
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
Expand All @@ -22,9 +23,11 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import timber.log.Timber

/**
* Primary implementation of [Fido2CredentialManager].
Expand All @@ -47,41 +50,45 @@ class Fido2CredentialManagerImpl(
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val clientData = if (fido2CreateCredentialRequest.callingAppInfo.isOriginPopulated()) {
fido2CreateCredentialRequest
.callingAppInfo
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
} else {
callingAppInfo
.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2RegisterCredentialResult.Error
} else {
ClientData.DefaultWithExtraData(
androidPackageName = fido2CreateCredentialRequest
.callingAppInfo
.packageName,
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
)
}
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
val host = getOriginUrlFromAttestationOptionsOrNull(
requestJson = fido2CreateCredentialRequest.requestJson,
)
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = host,
assetLinkUrl = host,
),
)
} else {
Origin.Web(fido2CreateCredentialRequest.origin)
}
val assetLinkUrl = fido2CreateCredentialRequest
.origin
?: getOriginUrlFromAttestationOptionsOrNull(fido2CreateCredentialRequest.requestJson)
?: return Fido2RegisterCredentialResult.Error

val origin = Origin.Android(
UnverifiedAssetLink(
packageName = fido2CreateCredentialRequest.packageName,
sha256CertFingerprint = fido2CreateCredentialRequest
.callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error,
host = assetLinkUrl.toHostOrPathOrNull()
?: return Fido2RegisterCredentialResult.Error,
assetLinkUrl = assetLinkUrl,
),
)
return vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = origin,
origin = sdkOrigin,
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
Expand All @@ -95,7 +102,11 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = { Fido2RegisterCredentialResult.Error },
onFailure = {
Fido2RegisterCredentialResult.Error(
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not have a Text inside the data package, that is a ui only class

)
},
)
}

Expand All @@ -114,8 +125,10 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
}

Expand All @@ -125,11 +138,14 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
}

@Suppress("LongMethod")
override suspend fun authenticateFido2Credential(
userId: String,
request: Fido2CredentialAssertionRequest,
Expand All @@ -139,39 +155,52 @@ class Fido2CredentialManagerImpl(
val clientData = request.clientDataHash
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
val origin = callingAppInfo.origin
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error
val relyingPartyId = json
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
?.relyingPartyId
?: return Fido2CredentialAssertionResult.Error
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
.asText(),
)

val validateOriginResult = validateOrigin(
callingAppInfo = callingAppInfo,
relyingPartyId = relyingPartyId,
)

val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
Origin.Web(request.origin)
} else {
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = hostUrl,
assetLinkUrl = hostUrl,
),
)
}

return when (validateOriginResult) {
is Fido2ValidateOriginResult.Error -> {
Fido2CredentialAssertionResult.Error
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
}

is Fido2ValidateOriginResult.Success -> {
vaultSdkSource
.authenticateFido2Credential(
request = AuthenticateFido2CredentialRequest(
userId = userId,
origin = Origin.Android(
UnverifiedAssetLink(
callingAppInfo.packageName,
callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error,
origin.toHostOrPathOrNull()
?: return Fido2CredentialAssertionResult.Error,
origin,
),
),
origin = sdkOrigin,
requestJson = """{"publicKey": ${request.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
Expand All @@ -183,7 +212,13 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
onFailure = { Fido2CredentialAssertionResult.Error },
onFailure = {
Timber.e(it, "Failed to authenticate FIDO2 credential.")
Fido2CredentialAssertionResult.Error(
R.string.passkey_authentication_failed_due_to_an_internal_error
.asText(),
)
},
)
}
}
Expand All @@ -195,13 +230,13 @@ class Fido2CredentialManagerImpl(
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
getPasskeyAssertionOptionsOrNull(requestJson)
?.relyingPartyId
?.let { "$HTTPS$it" }
?.prefixHttpsIfNecessaryOrNull()

private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
getPasskeyAttestationOptionsOrNull(requestJson)
?.relyingParty
?.id
?.let { "$HTTPS$it" }
?.prefixHttpsIfNecessaryOrNull()
}

private const val MAX_AUTHENTICATION_ATTEMPTS = 5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,4 @@ interface Fido2OriginManager {
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult

/**
* Returns the privileged app origin, or null if the calling app is not allowed.
*
* @param callingAppInfo The calling app info.
*
* @return The privileged app origin, or null.
*/
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ class Fido2OriginManagerImpl(
}
}

override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
if (!callingAppInfo.isOriginPopulated()) return null
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
?.takeUnless { !callingAppInfo.isOriginPopulated() }
}

private suspend fun validateCallingApplicationAssetLinks(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
Expand Down Expand Up @@ -123,7 +116,10 @@ class Fido2OriginManagerImpl(
}
.fold(
onSuccess = { it },
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
onFailure = {
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
Fido2ValidateOriginResult.Error.Unknown
},
)

/**
Expand Down Expand Up @@ -157,16 +153,4 @@ class Fido2OriginManagerImpl(
?: false
}
.takeUnless { it.isEmpty() }

private suspend fun getGoogleAllowListOrNull(): String? =
assetManager
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
.getOrNull()

private suspend fun getCommunityAllowListOrNull(): String? =
assetManager
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
.getOrNull()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.autofill.fido2.model

import com.x8bit.bitwarden.ui.platform.base.util.Text

/**
* Represents possible outcomes of a FIDO 2 credential assertion request.
*/
Expand All @@ -13,5 +15,5 @@ sealed class Fido2CredentialAssertionResult {
/**
* Indicates there was an error and the assertion was not successful.
*/
data object Error : Fido2CredentialAssertionResult()
data class Error(val message: Text) : Fido2CredentialAssertionResult()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't have this in the data package

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.model

import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.base.util.Text

/**
* Represents the result of a FIDO 2 Get Credentials request.
Expand All @@ -24,5 +25,7 @@ sealed class Fido2GetCredentialsResult {
/**
* Indicates an error was encountered when querying for matching credentials.
*/
data object Error : Fido2GetCredentialsResult()
data class Error(
val message: Text,
) : Fido2GetCredentialsResult()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.autofill.fido2.model

import com.x8bit.bitwarden.ui.platform.base.util.Text

/**
* Models the data returned from creating a FIDO 2 credential.
*/
Expand All @@ -9,13 +11,13 @@ sealed class Fido2RegisterCredentialResult {
* Indicates the credential has been successfully registered.
*/
data class Success(
val registrationResponse: String,
val responseJson: String,
) : Fido2RegisterCredentialResult()

/**
* Indicates there was an error and the credential was not registered.
*/
data object Error : Fido2RegisterCredentialResult()
data class Error(val message: Text) : Fido2RegisterCredentialResult()

/**
* Indicates the user cancelled the request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
@SerialName("id")
val id: String,
@SerialName("transports")
val transports: List<String>,
val transports: List<String>?,
)
Loading