mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 13:28:12 -04:00
Update Android passkey replace flow (#1685)
This commit is contained in:
committed by
Leendert de Borst
parent
10be2d3450
commit
d3e641c6e9
@@ -223,8 +223,8 @@ class AliasVaultCredentialProviderService : CredentialProviderService() {
|
||||
|
||||
// Extract RP info
|
||||
val rpObj = requestObj.optJSONObject("rp")
|
||||
val rpId = rpObj?.optString("id") ?: ""
|
||||
val rpName = rpObj?.optString("name") ?: rpId
|
||||
val rpId = rpObj?.optString("id")?.takeIf { it.isNotEmpty() } ?: ""
|
||||
val rpName = rpObj?.optString("name")?.takeIf { it.isNotEmpty() } ?: rpId
|
||||
|
||||
// Extract user info
|
||||
val userObj = requestObj.optJSONObject("user")
|
||||
@@ -233,23 +233,26 @@ class AliasVaultCredentialProviderService : CredentialProviderService() {
|
||||
|
||||
val createEntries = mutableListOf<CreateEntry>()
|
||||
|
||||
if (rpId.isNotEmpty()) {
|
||||
// Create entry for saving passkey to AliasVault
|
||||
// Using rpName or userDisplayName as the account name
|
||||
val accountName = if (userDisplayName.isNotEmpty()) {
|
||||
"$userDisplayName@$rpName"
|
||||
} else {
|
||||
rpName
|
||||
}
|
||||
|
||||
val entry = CreateEntry(
|
||||
accountName = accountName,
|
||||
pendingIntent = createNewPendingIntent(rpId),
|
||||
)
|
||||
|
||||
createEntries.add(entry)
|
||||
/*
|
||||
* Always create an entry for AliasVault, even if rpId is empty.
|
||||
* Per WebAuthn spec, rpId defaults to the origin's effective domain when not provided.
|
||||
* The actual rpId will be derived from the verified origin during registration.
|
||||
* Reference: PasskeyRegistrationActivity.kt derives rpId from origin if empty.
|
||||
*/
|
||||
val displayRpName = rpName.ifEmpty { "Passkey" }
|
||||
val accountName = if (userDisplayName.isNotEmpty()) {
|
||||
"$userDisplayName@$displayRpName"
|
||||
} else {
|
||||
displayRpName
|
||||
}
|
||||
|
||||
val entry = CreateEntry(
|
||||
accountName = accountName,
|
||||
pendingIntent = createNewPendingIntent(rpId.ifEmpty { "passkey-create" }),
|
||||
)
|
||||
|
||||
createEntries.add(entry)
|
||||
|
||||
return BeginCreateCredentialResponse(createEntries)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error handling passkey create query", e)
|
||||
|
||||
@@ -22,6 +22,7 @@ import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider
|
||||
import org.json.JSONObject
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* PasskeyRegistrationActivity
|
||||
@@ -80,17 +81,24 @@ class PasskeyRegistrationActivity : FragmentActivity() {
|
||||
|
||||
// Extract RP info
|
||||
val rpObj = requestObj.optJSONObject("rp")
|
||||
viewModel.rpId = rpObj?.optString("id") ?: ""
|
||||
viewModel.rpName = rpObj?.optString("name")?.takeIf { it.isNotEmpty() }
|
||||
|
||||
/*
|
||||
* Derive rpId: use explicit rp.id if provided, otherwise fall back to origin hostname.
|
||||
* This matches WebAuthn spec behavior where rpId defaults to the origin's effective domain.
|
||||
* Reference: browser extension PasskeyAuthenticator.ts and PasskeyCreate.tsx
|
||||
*/
|
||||
val explicitRpId = rpObj?.optString("id")?.takeIf { it.isNotEmpty() }
|
||||
viewModel.rpId = explicitRpId ?: ""
|
||||
|
||||
// Extract user info
|
||||
val userObj = requestObj.optJSONObject("user")
|
||||
viewModel.userName = userObj?.optString("name")?.takeIf { it.isNotEmpty() }
|
||||
viewModel.userDisplayName = userObj?.optString("displayName")?.takeIf { it.isNotEmpty() }
|
||||
val userIdB64 = userObj?.optString("id")
|
||||
|
||||
if (viewModel.rpId.isEmpty() || viewModel.requestJson.isEmpty()) {
|
||||
Log.e(TAG, "Missing required parameters")
|
||||
if (viewModel.requestJson.isEmpty()) {
|
||||
Log.e(TAG, "Missing required parameters: requestJson is empty")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
@@ -171,6 +179,23 @@ class PasskeyRegistrationActivity : FragmentActivity() {
|
||||
viewModel.isPrivilegedCaller = originResult.isPrivileged
|
||||
Log.d(TAG, "Origin verified: ${originResult.origin} (privileged: ${originResult.isPrivileged})")
|
||||
|
||||
/*
|
||||
* If rpId was not provided in the request, derive it from the verified origin.
|
||||
* This matches WebAuthn spec behavior where rpId defaults to origin's effective domain.
|
||||
* Reference: browser extension PasskeyAuthenticator.ts and PasskeyCreate.tsx
|
||||
*/
|
||||
if (viewModel.rpId.isEmpty() && originResult.isPrivileged) {
|
||||
try {
|
||||
val originUrl = URL(originResult.origin)
|
||||
viewModel.rpId = originUrl.host
|
||||
Log.d(TAG, "Derived rpId from origin: ${viewModel.rpId}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to derive rpId from origin", e)
|
||||
showError("Invalid origin URL")
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize unlock coordinator
|
||||
unlockCoordinator = UnlockCoordinator(
|
||||
activity = this@PasskeyRegistrationActivity,
|
||||
@@ -216,7 +241,6 @@ class PasskeyRegistrationActivity : FragmentActivity() {
|
||||
// Get existing passkeys for the rpId (can be replaced)
|
||||
viewModel.existingPasskeys = vaultStore.getPasskeysWithCredentialInfo(
|
||||
rpId = viewModel.rpId,
|
||||
userName = viewModel.userName,
|
||||
userId = viewModel.userId,
|
||||
)
|
||||
|
||||
|
||||
@@ -285,7 +285,6 @@ class PasskeyRepository(database: VaultDatabase) : BaseRepository(database) {
|
||||
* @param displayName The updated display name.
|
||||
* @param logo The updated logo bytes (optional).
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER") // Logo update not yet implemented
|
||||
fun replace(
|
||||
oldPasskeyId: UUID,
|
||||
newPasskey: Passkey,
|
||||
@@ -304,11 +303,31 @@ class PasskeyRepository(database: VaultDatabase) : BaseRepository(database) {
|
||||
|
||||
// Update the item's name
|
||||
executeUpdate(
|
||||
"UPDATE Items SET Name = ?, UpdatedAt = ? WHERE Id = ?",
|
||||
arrayOf(displayName, timestamp, itemId.toString().uppercase()),
|
||||
"UPDATE Items SET UpdatedAt = ? WHERE Id = ?",
|
||||
arrayOf(timestamp, itemId.toString().uppercase()),
|
||||
)
|
||||
|
||||
// TODO: Update logo if provided
|
||||
if (logo != null) {
|
||||
val source = newPasskey.rpId.lowercase().replace("www.", "")
|
||||
val itemResults = executeQuery(
|
||||
"SELECT LogoId FROM Items WHERE Id = ?",
|
||||
arrayOf(itemId.toString().uppercase()),
|
||||
)
|
||||
val existingLogoId = itemResults.firstOrNull()?.get("LogoId") as? String
|
||||
|
||||
if (existingLogoId != null) {
|
||||
executeUpdate(
|
||||
"UPDATE Logos SET FileData = ?, UpdatedAt = ? WHERE Id = ?",
|
||||
arrayOf(logo, timestamp, existingLogoId),
|
||||
)
|
||||
} else {
|
||||
val newLogoId = getOrCreateLogo(source, logo, timestamp)
|
||||
executeUpdate(
|
||||
"UPDATE Items SET LogoId = ?, UpdatedAt = ? WHERE Id = ?",
|
||||
arrayOf(newLogoId, timestamp, itemId.toString().uppercase()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Soft delete the old passkey
|
||||
softDelete("Passkeys", oldPasskeyId.toString().uppercase())
|
||||
|
||||
Reference in New Issue
Block a user