From 4cea8aae5e2fad8407613500ab874bb1024d59b5 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 21 Oct 2025 15:17:58 +0200 Subject: [PATCH] Fix passkey create in Firefox Android (#520) --- .../src/entrypoints/webauthn.ts | 2 +- .../AliasVaultCredentialProviderService.kt | 32 +----- .../PasskeyRegistrationActivity.kt | 106 +++++++++--------- 3 files changed, 58 insertions(+), 82 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/webauthn.ts b/apps/browser-extension/src/entrypoints/webauthn.ts index 444357ad5..a2758da40 100644 --- a/apps/browser-extension/src/entrypoints/webauthn.ts +++ b/apps/browser-extension/src/entrypoints/webauthn.ts @@ -184,7 +184,7 @@ export default defineUnlistedScript(() => { clientDataJSON: base64ToBuffer(cred.clientDataJSON), attestationObject: attestationObjectBuffer, /** - * getTransports + * getTransports TODO: delete this, not used for cross-platform? */ getTransports() : string[] { return ['internal']; diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/AliasVaultCredentialProviderService.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/AliasVaultCredentialProviderService.kt index 6a26ccb87..234c49444 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/AliasVaultCredentialProviderService.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/AliasVaultCredentialProviderService.kt @@ -41,13 +41,6 @@ class AliasVaultCredentialProviderService : CredentialProviderService() { const val EXTRA_REQUEST_JSON = "request_json" const val EXTRA_RP_ID = "rp_id" const val EXTRA_PASSKEY_ID = "passkey_id" - - // Intent extras for registration - const val EXTRA_CREATE_REQUEST_JSON = "create_request_json" - const val EXTRA_CREATE_RP_ID = "create_rp_id" - const val EXTRA_CREATE_USER_NAME = "create_user_name" - const val EXTRA_CREATE_USER_DISPLAY_NAME = "create_user_display_name" - const val EXTRA_CREATE_USER_ID = "create_user_id" } /** @@ -291,13 +284,7 @@ class AliasVaultCredentialProviderService : CredentialProviderService() { val entry = CreateEntry( accountName = accountName, - pendingIntent = createNewPendingIntent( - rpId = rpId, - userName = userName.ifEmpty { null }, - userDisplayName = userDisplayName.ifEmpty { null }, - userIdB64 = userIdB64.ifEmpty { null }, - requestJson = requestJson, - ), + pendingIntent = createNewPendingIntent(rpId), ) createEntries.add(entry) @@ -313,22 +300,11 @@ class AliasVaultCredentialProviderService : CredentialProviderService() { /** * Create a PendingIntent for passkey registration + * The intent doesn't need any extras - all data is available via providerRequest.callingRequest */ - private fun createNewPendingIntent( - rpId: String, - userName: String?, - userDisplayName: String?, - userIdB64: String?, - requestJson: String, - ): PendingIntent { + private fun createNewPendingIntent(rpId: String): PendingIntent { // Create intent for PasskeyRegistrationActivity - val intent = Intent(this, PasskeyRegistrationActivity::class.java).apply { - putExtra(EXTRA_CREATE_REQUEST_JSON, requestJson) - putExtra(EXTRA_CREATE_RP_ID, rpId) - userName?.let { putExtra(EXTRA_CREATE_USER_NAME, it) } - userDisplayName?.let { putExtra(EXTRA_CREATE_USER_DISPLAY_NAME, it) } - userIdB64?.let { putExtra(EXTRA_CREATE_USER_ID, it) } - } + val intent = Intent(this, PasskeyRegistrationActivity::class.java) return PendingIntent.getActivity( this, diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/PasskeyRegistrationActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/PasskeyRegistrationActivity.kt index fbbb9c0de..897129371 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/PasskeyRegistrationActivity.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialprovider/PasskeyRegistrationActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.util.Log import android.view.View import android.widget.TextView +import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePublicKeyCredentialResponse import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.ProviderCreateCredentialRequest @@ -18,19 +19,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.aliasvault.app.R import net.aliasvault.app.components.LoadingIndicator -import net.aliasvault.app.credentialprovider.AliasVaultCredentialProviderService.Companion.EXTRA_CREATE_REQUEST_JSON -import net.aliasvault.app.credentialprovider.AliasVaultCredentialProviderService.Companion.EXTRA_CREATE_RP_ID -import net.aliasvault.app.credentialprovider.AliasVaultCredentialProviderService.Companion.EXTRA_CREATE_USER_DISPLAY_NAME -import net.aliasvault.app.credentialprovider.AliasVaultCredentialProviderService.Companion.EXTRA_CREATE_USER_ID -import net.aliasvault.app.credentialprovider.AliasVaultCredentialProviderService.Companion.EXTRA_CREATE_USER_NAME import net.aliasvault.app.vaultstore.VaultStore import net.aliasvault.app.vaultstore.createCredentialWithPasskey import net.aliasvault.app.vaultstore.models.Passkey import net.aliasvault.app.vaultstore.passkey.PasskeyAuthenticator import net.aliasvault.app.vaultstore.passkey.PasskeyHelper import net.aliasvault.app.webapi.WebApiService +import org.json.JSONArray import org.json.JSONObject -import java.security.MessageDigest import java.util.Date import java.util.UUID @@ -66,6 +62,8 @@ class PasskeyRegistrationActivity : Activity() { // Request data private var providerRequest: ProviderCreateCredentialRequest? = null private var requestJson: String = "" + private var clientDataHash: ByteArray? = null + private var origin: String? = null private var rpId: String = "" private var userName: String? = null private var userDisplayName: String? = null @@ -98,12 +96,35 @@ class PasskeyRegistrationActivity : Activity() { Log.d(TAG, "Provider request retrieved successfully") - // Extract parameters from intent - requestJson = intent.getStringExtra(EXTRA_CREATE_REQUEST_JSON) ?: "" - rpId = intent.getStringExtra(EXTRA_CREATE_RP_ID) ?: "" - userName = intent.getStringExtra(EXTRA_CREATE_USER_NAME) - userDisplayName = intent.getStringExtra(EXTRA_CREATE_USER_DISPLAY_NAME) - val userIdB64 = intent.getStringExtra(EXTRA_CREATE_USER_ID) + // Extract parameters from providerRequest.callingRequest + val createRequest = providerRequest!!.callingRequest + if (createRequest !is CreatePublicKeyCredentialRequest) { + Log.e(TAG, "Request is not a CreatePublicKeyCredentialRequest") + showError(getString(R.string.passkey_creation_failed)) + return + } + + // Get requestJson, clientDataHash, and origin from the request + requestJson = createRequest.requestJson + clientDataHash = createRequest.clientDataHash + origin = createRequest.origin + + Log.d(TAG, "Request JSON: $requestJson") + Log.d(TAG, "Origin: $origin") + Log.d(TAG, "ClientDataHash length: ${clientDataHash?.size}") + + // Parse request JSON to extract RP ID and user info + val requestObj = JSONObject(requestJson) + + // Extract RP info + val rpObj = requestObj.optJSONObject("rp") + rpId = rpObj?.optString("id") ?: "" + + // Extract user info + val userObj = requestObj.optJSONObject("user") + userName = userObj?.optString("name")?.takeIf { it.isNotEmpty() } + userDisplayName = userObj?.optString("displayName")?.takeIf { it.isNotEmpty() } + val userIdB64 = userObj?.optString("id") Log.d(TAG, "Parameters: rpId=$rpId, userName=$userName, userDisplayName=$userDisplayName") @@ -228,9 +249,6 @@ class PasskeyRegistrationActivity : Activity() { showLoading(getString(R.string.passkey_creating)) } - // wait for 10 seconds to test interface - delay(10000) - Log.d(TAG, "Creating passkey for RP: $rpId, user: $userName") // Extract favicon (optional) @@ -246,18 +264,22 @@ class PasskeyRegistrationActivity : Activity() { val passkeyId = UUID.randomUUID() val credentialId = PasskeyHelper.guidToBytes(passkeyId.toString()) - // Parse request to get challenge + // Use clientDataHash from the request + val requestClientDataHash = this@PasskeyRegistrationActivity.clientDataHash + if (requestClientDataHash == null) { + throw Exception("Client data hash not available") + } + + // Parse request to get challenge (for building response clientDataJSON later) val requestObj = JSONObject(requestJson) val challenge = requestObj.optString("challenge", "") - // Construct origin from calling app signing certificate - val origin = appInfoToOrigin(providerRequest!!.callingAppInfo) - Log.d(TAG, "Origin: $origin") - - // Build clientDataJSON - val clientDataJson = - """{"type":"webauthn.create","challenge":"$challenge","origin":"$origin","crossOrigin":false}""" - val clientDataHash = sha256(clientDataJson.toByteArray(Charsets.UTF_8)) + // Use origin from the request + val requestOrigin = this@PasskeyRegistrationActivity.origin + if (requestOrigin == null) { + throw Exception("Origin not available") + } + Log.d(TAG, "Using origin from request: $requestOrigin") // Extract PRF inputs if present val prfInputs = extractPrfInputs(requestObj) @@ -266,7 +288,7 @@ class PasskeyRegistrationActivity : Activity() { // Create the passkey using PasskeyAuthenticator val passkeyResult = PasskeyAuthenticator.createPasskey( credentialId = credentialId, - clientDataHash = clientDataHash, + clientDataHash = requestClientDataHash, rpId = rpId, userId = userId, userName = userName, @@ -327,28 +349,25 @@ class PasskeyRegistrationActivity : Activity() { // Build response val credentialIdB64 = base64urlEncode(credentialId) val attestationObjectB64 = base64urlEncode(passkeyResult.attestationObject) + + // Rebuild clientDataJSON for the response (needed for the credential response) + val clientDataJson = + """{"type":"webauthn.create","challenge":"$challenge","origin":"$requestOrigin","crossOrigin":false}""" val clientDataJsonB64 = base64urlEncode(clientDataJson.toByteArray(Charsets.UTF_8)) val responseJson = JSONObject().apply { put("id", credentialIdB64) put("rawId", credentialIdB64) put("type", "public-key") - put("authenticatorAttachment", "platform") - + put("authenticatorAttachment", "cross-platform") put( "response", JSONObject().apply { put("clientDataJSON", clientDataJsonB64) put("attestationObject", attestationObjectB64) - put( - "transports", - org.json.JSONArray().apply { - put("internal") - }, - ) + put("transports", JSONArray().apply { put("hybrid") }) }, ) - // Add PRF extension results if present val prfResults = if (enablePrf) passkeyResult.prfResults else null if (prfResults != null) { @@ -429,14 +448,6 @@ class PasskeyRegistrationActivity : Activity() { } } - /** - * Compute SHA-256 hash - */ - private fun sha256(data: ByteArray): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(data) - } - /** * Encode bytes to base64url string */ @@ -463,15 +474,4 @@ class PasskeyRegistrationActivity : Activity() { return android.util.Base64.decode(base64, android.util.Base64.NO_WRAP) } - - /** - * Compute the origin from CallingAppInfo - * Format: android:apk-key-hash: - */ - private fun appInfoToOrigin(info: androidx.credentials.provider.CallingAppInfo): String { - val cert = info.signingInfo.apkContentsSigners[0].toByteArray() - val md = MessageDigest.getInstance("SHA-256") - val certHash = md.digest(cert) - return "android:apk-key-hash:${base64urlEncode(certHash)}" - } }