mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-10 02:42:47 -04:00
Simplify PasskeyAuthenticationActivity.kt (#520)
This commit is contained in:
@@ -7,7 +7,7 @@ import android.util.Log
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.models.Passkey
|
||||
import net.aliasvault.app.vaultstore.getPasskeyById
|
||||
import net.aliasvault.app.vaultstore.passkey.PasskeyAuthenticator
|
||||
import net.aliasvault.app.vaultstore.passkey.PasskeyHelper
|
||||
import org.json.JSONObject
|
||||
@@ -98,7 +98,7 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
return
|
||||
}
|
||||
|
||||
val passkey = getPasskeyById(passkeyId, db, vaultStore)
|
||||
val passkey = vaultStore.getPasskeyById(passkeyId, db)
|
||||
if (passkey == null) {
|
||||
Log.e(TAG, "Passkey not found: $passkeyId")
|
||||
setResult(RESULT_CANCELED)
|
||||
@@ -111,11 +111,7 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
) ?: ""
|
||||
val requestObj = JSONObject(requestJson)
|
||||
|
||||
val rpId = passkey.rpId
|
||||
val origin = requestObj.optString("origin", "https://$rpId")
|
||||
|
||||
// Extract clientDataHash from Chrome's request
|
||||
// Chrome provides a pre-computed clientDataHash that we must use for signing
|
||||
var clientDataHashFromChrome: ByteArray? = null
|
||||
providerRequest.credentialOptions.forEach { option ->
|
||||
if (option is androidx.credentials.GetPublicKeyCredentialOption) {
|
||||
@@ -123,18 +119,39 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
// If Chrome didn't provide clientDataHash, build clientDataJSON and hash it
|
||||
val clientDataHash: ByteArray
|
||||
val clientDataJson: String?
|
||||
if (clientDataHashFromChrome != null) {
|
||||
clientDataHash = clientDataHashFromChrome
|
||||
// Don't build clientDataJSON - Chrome has its own
|
||||
clientDataJson = null
|
||||
} else {
|
||||
val challenge = requestObj.optString("challenge", "")
|
||||
val origin = requestObj.optString("origin", "https://${passkey.rpId}")
|
||||
val json = buildClientDataJson(challenge, origin)
|
||||
clientDataHash = sha256(json.toByteArray(Charsets.UTF_8))
|
||||
clientDataJson = json
|
||||
}
|
||||
|
||||
// Use PasskeyAuthenticator.getAssertion for signing
|
||||
val credentialId = PasskeyHelper.guidToBytes(passkey.id.toString())
|
||||
val prfInputs = extractPrfInputs(requestObj)
|
||||
val assertion = PasskeyAuthenticator.getAssertion(
|
||||
credentialId = credentialId,
|
||||
clientDataHash = clientDataHash,
|
||||
rpId = passkey.rpId,
|
||||
privateKeyJWK = passkey.privateKey,
|
||||
userId = passkey.userHandle,
|
||||
uvPerformed = true,
|
||||
prfInputs = prfInputs,
|
||||
prfSecret = passkey.prfKey,
|
||||
)
|
||||
|
||||
val response = buildAuthenticationResponseWithSignature(
|
||||
requestJson,
|
||||
credentialId,
|
||||
passkey.privateKey,
|
||||
passkey.userHandle,
|
||||
origin,
|
||||
prfInputs,
|
||||
passkey.prfKey,
|
||||
clientDataHashFromChrome,
|
||||
// Build response JSON
|
||||
val response = buildPublicKeyCredentialResponse(
|
||||
assertion = assertion,
|
||||
clientDataJson = clientDataJson,
|
||||
)
|
||||
|
||||
val resultIntent = Intent()
|
||||
@@ -163,89 +180,7 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get passkey by its UUID (not credential ID)
|
||||
*/
|
||||
private fun getPasskeyById(
|
||||
passkeyId: UUID,
|
||||
db: android.database.sqlite.SQLiteDatabase,
|
||||
vaultStore: VaultStore,
|
||||
): net.aliasvault.app.vaultstore.models.Passkey? {
|
||||
val query = """
|
||||
SELECT Id, CredentialId, RpId, UserHandle, PublicKey, PrivateKey, PrfKey,
|
||||
DisplayName, CreatedAt, UpdatedAt, IsDeleted
|
||||
FROM Passkeys
|
||||
WHERE Id = ? AND IsDeleted = 0
|
||||
LIMIT 1
|
||||
""".trimIndent()
|
||||
|
||||
Log.d(TAG, "Querying for passkey with ID: $passkeyId")
|
||||
|
||||
val cursor = db.rawQuery(query, arrayOf(passkeyId.toString().uppercase()))
|
||||
cursor.use {
|
||||
if (it.moveToFirst()) {
|
||||
Log.d(TAG, "Found passkey in database")
|
||||
return parsePasskeyRow(it)
|
||||
} else {
|
||||
Log.w(TAG, "Passkey not found in database. Checking all passkeys...")
|
||||
// Debug: List all passkeys to see what's in the database
|
||||
val debugQuery = "SELECT Id, RpId, DisplayName FROM Passkeys WHERE IsDeleted = 0"
|
||||
val debugCursor = db.rawQuery(debugQuery, null)
|
||||
debugCursor.use { debugIt ->
|
||||
var count = 0
|
||||
while (debugIt.moveToNext()) {
|
||||
count++
|
||||
val id = debugIt.getString(0)
|
||||
val rpId = debugIt.getString(1)
|
||||
val displayName = debugIt.getString(2)
|
||||
Log.d(TAG, "Passkey $count: ID=$id, RpId=$rpId, DisplayName=$displayName")
|
||||
}
|
||||
Log.d(TAG, "Total passkeys in database: $count")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse passkey from cursor (simplified version from VaultStorePasskey)
|
||||
*/
|
||||
private fun parsePasskeyRow(cursor: android.database.Cursor): net.aliasvault.app.vaultstore.models.Passkey? {
|
||||
try {
|
||||
val id = UUID.fromString(cursor.getString(0))
|
||||
val parentCredentialId = UUID.fromString(cursor.getString(1))
|
||||
val rpId = cursor.getString(2)
|
||||
val userHandle = if (!cursor.isNull(3)) cursor.getBlob(3) else null
|
||||
val publicKey = cursor.getString(4).toByteArray(Charsets.UTF_8)
|
||||
val privateKey = cursor.getString(5).toByteArray(Charsets.UTF_8)
|
||||
val prfKey = if (!cursor.isNull(6)) cursor.getBlob(6) else null
|
||||
val displayName = cursor.getString(7)
|
||||
|
||||
// Use current date for createdAt/updatedAt as we don't need them here
|
||||
val now = java.util.Date()
|
||||
|
||||
return net.aliasvault.app.vaultstore.models.Passkey(
|
||||
id = id,
|
||||
parentCredentialId = parentCredentialId,
|
||||
rpId = rpId,
|
||||
userHandle = userHandle,
|
||||
userName = null,
|
||||
publicKey = publicKey,
|
||||
privateKey = privateKey,
|
||||
prfKey = prfKey,
|
||||
displayName = displayName,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
isDeleted = false,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing passkey row", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build clientDataJSON for WebAuthn (from challenge ByteArray)
|
||||
* Build clientDataJSON for WebAuthn
|
||||
*/
|
||||
private fun buildClientDataJson(challenge: ByteArray, origin: String): String {
|
||||
val challengeB64 = base64urlEncode(challenge)
|
||||
@@ -254,26 +189,14 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
|
||||
/**
|
||||
* Build clientDataJSON for WebAuthn
|
||||
*
|
||||
* IMPORTANT: Per WebAuthn spec (https://www.w3.org/TR/webauthn-2/#clientdatajson-serialization),
|
||||
* the server/RP MUST parse clientDataJSON as JSON and validate only the required fields.
|
||||
* Browsers (Chrome) may add extra fields like "other_keys_can_be_added_here" to test that
|
||||
* RPs don't do naive template matching.
|
||||
*
|
||||
* We build a minimal, spec-compliant clientDataJSON here. The server should:
|
||||
* 1. Parse as JSON (not string compare!)
|
||||
* 2. Validate type === "webauthn.get"
|
||||
* 3. Validate challenge matches expected value
|
||||
* 4. Validate origin is allowed for this RP
|
||||
* 5. Ignore any extra fields
|
||||
*
|
||||
* CRITICAL: Must NOT escape forward slashes in origin!
|
||||
* JavaScript JSON.stringify() doesn't escape slashes by default, but Android's JSONObject does.
|
||||
*/
|
||||
private fun buildClientDataJson(challenge: String, origin: String): String {
|
||||
// Build JSON manually WITHOUT escaping forward slashes
|
||||
// This matches browser behavior where JSON.stringify() doesn't escape slashes
|
||||
// NOTE: We only include required fields. Extra fields from browsers are allowed by spec.
|
||||
// TODO: check if this point is ever hit?
|
||||
Log.d(TAG, "--------------------------------------")
|
||||
Log.d(TAG, "Building clientDataJSON for WebAuthn manually")
|
||||
Log.d(TAG, "--------------------------------------")
|
||||
return """{"type":"webauthn.get","challenge":"$challenge","origin":"$origin","crossOrigin":false}"""
|
||||
}
|
||||
|
||||
@@ -339,79 +262,20 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WebAuthn assertion response with proper signature
|
||||
* Build PublicKeyCredential response from PasskeyAuthenticator assertion result
|
||||
*
|
||||
* CRITICAL: If Chrome provides clientDataHash, we MUST use that for signing.
|
||||
* Otherwise, we build our own clientDataJSON.
|
||||
*
|
||||
* Chrome on Android will replace our clientDataJSON with its own (including extra fields),
|
||||
* so we must sign using Chrome's pre-computed hash to ensure the signature matches.
|
||||
* @param assertion The assertion result from PasskeyAuthenticator.getAssertion
|
||||
* @param clientDataJson Optional clientDataJSON string. If null, clientDataJSON will be
|
||||
* omitted from the response (used when Chrome provides clientDataHash)
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildAuthenticationResponseWithSignature(
|
||||
requestJson: String,
|
||||
credentialId: ByteArray,
|
||||
privateKeyJWK: ByteArray,
|
||||
userHandle: ByteArray?,
|
||||
origin: String,
|
||||
prfInputs: PasskeyAuthenticator.PrfInputs?,
|
||||
prfSecret: ByteArray?,
|
||||
clientDataHashFromChrome: ByteArray?,
|
||||
private fun buildPublicKeyCredentialResponse(
|
||||
assertion: PasskeyAuthenticator.PasskeyAssertionResult,
|
||||
clientDataJson: String?,
|
||||
): androidx.credentials.GetCredentialResponse {
|
||||
val requestObj = JSONObject(requestJson)
|
||||
val challengeB64 = requestObj.optString("challenge", "")
|
||||
|
||||
// Determine clientDataHash - use Chrome's if provided, otherwise build our own
|
||||
val clientDataHash: ByteArray
|
||||
val clientDataB64: String
|
||||
|
||||
if (clientDataHashFromChrome != null) {
|
||||
clientDataHash = clientDataHashFromChrome
|
||||
val clientDataJson = buildClientDataJson(challengeB64, origin)
|
||||
clientDataB64 = base64urlEncode(clientDataJson.toByteArray(Charsets.UTF_8))
|
||||
} else {
|
||||
val clientDataJson = buildClientDataJson(challengeB64, origin)
|
||||
val clientDataBytes = clientDataJson.toByteArray(Charsets.UTF_8)
|
||||
clientDataHash = sha256(clientDataBytes)
|
||||
clientDataB64 = base64urlEncode(clientDataBytes)
|
||||
}
|
||||
|
||||
val rpId = requestObj.optString("rpId", "")
|
||||
if (rpId.isEmpty()) {
|
||||
throw IllegalArgumentException("rpId required in request")
|
||||
}
|
||||
|
||||
val userVerified = true
|
||||
val authData = buildAuthenticatorData(
|
||||
rpId = rpId,
|
||||
userVerified = userVerified,
|
||||
includeExtensions = false,
|
||||
)
|
||||
val authDataB64 = base64urlEncode(authData)
|
||||
|
||||
val dataToSign = authData + clientDataHash
|
||||
val privateKey = importPrivateKeyFromJWK(privateKeyJWK)
|
||||
|
||||
val signer = java.security.Signature.getInstance("SHA256withECDSA")
|
||||
signer.initSign(privateKey)
|
||||
signer.update(dataToSign)
|
||||
val rawSignature = signer.sign()
|
||||
|
||||
val signature = canonicalizeECDSASignature(rawSignature)
|
||||
|
||||
// Step 6: Evaluate PRF extension if requested
|
||||
val prfResults = if (prfInputs?.first != null && prfSecret != null) {
|
||||
val first = evaluatePrf(prfSecret, prfInputs.first!!)
|
||||
val second = prfInputs.second?.let { evaluatePrf(prfSecret, it) }
|
||||
PasskeyAuthenticator.PrfResults(first, second)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Build WebAuthn assertion response JSON
|
||||
val credentialIdB64 = base64urlEncode(credentialId)
|
||||
val signatureB64 = base64urlEncode(signature)
|
||||
val userHandleB64 = userHandle?.let { base64urlEncode(it) }
|
||||
val credentialIdB64 = base64urlEncode(assertion.credentialId)
|
||||
val signatureB64 = base64urlEncode(assertion.signature)
|
||||
val authDataB64 = base64urlEncode(assertion.authenticatorData)
|
||||
val userHandleB64 = assertion.userHandle?.let { base64urlEncode(it) }
|
||||
|
||||
val responseObj = JSONObject().apply {
|
||||
put("id", credentialIdB64)
|
||||
@@ -422,7 +286,11 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
put(
|
||||
"response",
|
||||
JSONObject().apply {
|
||||
put("clientDataJSON", clientDataB64)
|
||||
// Only include clientDataJSON if we built it ourselves
|
||||
// When Chrome provides clientDataHash, omit this field
|
||||
clientDataJson?.let {
|
||||
put("clientDataJSON", base64urlEncode(it.toByteArray(Charsets.UTF_8)))
|
||||
}
|
||||
put("authenticatorData", authDataB64)
|
||||
put("signature", signatureB64)
|
||||
userHandleB64?.let { put("userHandle", it) }
|
||||
@@ -431,7 +299,7 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
|
||||
put(
|
||||
"clientExtensionResults",
|
||||
prfResults?.let {
|
||||
assertion.prfResults?.let {
|
||||
JSONObject().apply {
|
||||
put(
|
||||
"prf",
|
||||
@@ -454,201 +322,4 @@ class PasskeyAuthenticationActivity : Activity() {
|
||||
androidx.credentials.PublicKeyCredential(responseObj.toString()),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build authenticatorData deterministically per WebAuthn spec
|
||||
*
|
||||
* @param rpId The relying party ID
|
||||
* @param userVerified Whether user verification was performed
|
||||
* @param includeExtensions Whether to include extension data
|
||||
* @return The authenticatorData bytes
|
||||
*/
|
||||
private fun buildAuthenticatorData(
|
||||
rpId: String,
|
||||
userVerified: Boolean,
|
||||
includeExtensions: Boolean = false,
|
||||
): ByteArray {
|
||||
// rpIdHash: SHA-256(rpId)
|
||||
val rpIdHash = sha256(rpId.toByteArray(Charsets.UTF_8))
|
||||
|
||||
// Flags byte (bit 0 = UP, bit 2 = UV, bit 7 = ED)
|
||||
var flags: Byte = 0x01 // UP (User Present) = 1
|
||||
if (userVerified) {
|
||||
flags = (flags.toInt() or 0x04).toByte() // UV (User Verified) = 1
|
||||
}
|
||||
if (includeExtensions) {
|
||||
flags = (flags.toInt() or 0x80).toByte() // ED (Extension Data) = 1
|
||||
}
|
||||
// BE (Backup Eligible) and BS (Backup State) = 0 (bits 3 and 4)
|
||||
// We're not a synced/backup authenticator for credential manager purposes
|
||||
|
||||
// signCount: 4 bytes, big-endian, set to 0
|
||||
// (we don't maintain per-credential counters)
|
||||
val signCount = byteArrayOf(0x00, 0x00, 0x00, 0x00)
|
||||
|
||||
// Extensions: empty CBOR map if ED flag is set
|
||||
// PRF results go in clientExtensionResults, not authenticator extensions
|
||||
val extensions = if (includeExtensions) {
|
||||
// Empty CBOR map: 0xA0
|
||||
byteArrayOf(0xA0.toByte())
|
||||
} else {
|
||||
byteArrayOf()
|
||||
}
|
||||
|
||||
return rpIdHash + flags + signCount + extensions
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize ECDSA signature to low-S form per WebAuthn spec
|
||||
*/
|
||||
private fun canonicalizeECDSASignature(derSignature: ByteArray): ByteArray {
|
||||
// Parse DER-encoded signature
|
||||
val (r, s) = parseDERSignature(derSignature)
|
||||
|
||||
// secp256r1 order (n)
|
||||
val n = java.math.BigInteger(
|
||||
"FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551",
|
||||
16,
|
||||
)
|
||||
|
||||
val halfN = n.shiftRight(1)
|
||||
|
||||
val canonicalS = if (s > halfN) n.subtract(s) else s
|
||||
|
||||
// Re-encode to DER
|
||||
return encodeDERSignature(r, canonicalS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse DER-encoded ECDSA signature into (r, s) components
|
||||
*/
|
||||
private fun parseDERSignature(der: ByteArray): Pair<java.math.BigInteger, java.math.BigInteger> {
|
||||
var offset = 0
|
||||
|
||||
// SEQUENCE tag
|
||||
require(der[offset++].toInt() == 0x30) { "Invalid DER: expected SEQUENCE" }
|
||||
|
||||
// SEQUENCE length
|
||||
val seqLength = der[offset++].toInt() and 0xFF
|
||||
require(seqLength == der.size - 2) { "Invalid DER: incorrect SEQUENCE length" }
|
||||
|
||||
// INTEGER tag for r
|
||||
require(der[offset++].toInt() == 0x02) { "Invalid DER: expected INTEGER for r" }
|
||||
val rLength = der[offset++].toInt() and 0xFF
|
||||
val rBytes = der.copyOfRange(offset, offset + rLength)
|
||||
offset += rLength
|
||||
val r = java.math.BigInteger(1, rBytes)
|
||||
|
||||
// INTEGER tag for s
|
||||
require(der[offset++].toInt() == 0x02) { "Invalid DER: expected INTEGER for s" }
|
||||
val sLength = der[offset++].toInt() and 0xFF
|
||||
val sBytes = der.copyOfRange(offset, offset + sLength)
|
||||
val s = java.math.BigInteger(1, sBytes)
|
||||
|
||||
return Pair(r, s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode (r, s) components into DER-encoded ECDSA signature
|
||||
*/
|
||||
private fun encodeDERSignature(r: java.math.BigInteger, s: java.math.BigInteger): ByteArray {
|
||||
fun encodeInteger(value: java.math.BigInteger): ByteArray {
|
||||
val bytes = value.toByteArray()
|
||||
// BigInteger.toByteArray() includes sign byte if MSB is set
|
||||
// DER INTEGER should have minimal encoding
|
||||
return byteArrayOf(0x02.toByte(), bytes.size.toByte()) + bytes
|
||||
}
|
||||
|
||||
val rEncoded = encodeInteger(r)
|
||||
val sEncoded = encodeInteger(s)
|
||||
|
||||
val totalLength = rEncoded.size + sEncoded.size
|
||||
|
||||
return byteArrayOf(
|
||||
0x30.toByte(), // SEQUENCE tag
|
||||
totalLength.toByte(), // SEQUENCE length
|
||||
) + rEncoded + sEncoded
|
||||
}
|
||||
|
||||
/**
|
||||
* Import public key from private key JWK (uses x, y coordinates)
|
||||
*/
|
||||
private fun importPublicKeyFromPrivateJWK(jwkData: ByteArray): java.security.interfaces.ECPublicKey {
|
||||
val jwkString = String(jwkData, Charsets.UTF_8)
|
||||
val jwk = JSONObject(jwkString)
|
||||
|
||||
// Extract x and y coordinates
|
||||
val xBase64url = jwk.optString("x") ?: throw IllegalArgumentException("Missing 'x' in JWK")
|
||||
val yBase64url = jwk.optString("y") ?: throw IllegalArgumentException("Missing 'y' in JWK")
|
||||
|
||||
val xBytes = base64urlDecode(xBase64url)
|
||||
val yBytes = base64urlDecode(yBase64url)
|
||||
|
||||
// Create ECPoint
|
||||
val x = java.math.BigInteger(1, xBytes)
|
||||
val y = java.math.BigInteger(1, yBytes)
|
||||
val point = java.security.spec.ECPoint(x, y)
|
||||
|
||||
// Get P-256 curve parameters
|
||||
val ecSpec = java.security.spec.ECGenParameterSpec("secp256r1")
|
||||
val params = java.security.AlgorithmParameters.getInstance("EC")
|
||||
params.init(ecSpec)
|
||||
val ecParameterSpec = params.getParameterSpec(java.security.spec.ECParameterSpec::class.java)
|
||||
|
||||
// Create ECPublicKeySpec
|
||||
val pubKeySpec = java.security.spec.ECPublicKeySpec(point, ecParameterSpec)
|
||||
|
||||
// Generate the public key
|
||||
val keyFactory = java.security.KeyFactory.getInstance("EC")
|
||||
return keyFactory.generatePublic(pubKeySpec) as java.security.interfaces.ECPublicKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Import private key from JWK format (duplicate from PasskeyAuthenticator for local use)
|
||||
*/
|
||||
private fun importPrivateKeyFromJWK(jwkData: ByteArray): java.security.interfaces.ECPrivateKey {
|
||||
val jwkString = String(jwkData, Charsets.UTF_8)
|
||||
val jwk = JSONObject(jwkString)
|
||||
|
||||
// Extract the d parameter (private key component)
|
||||
val dBase64url = jwk.optString("d")
|
||||
?: throw IllegalArgumentException("Missing 'd' parameter in JWK")
|
||||
|
||||
// Decode base64url to bytes
|
||||
val dBytes = base64urlDecode(dBase64url)
|
||||
|
||||
// Convert to BigInteger (d parameter is the private key value)
|
||||
val d = java.math.BigInteger(1, dBytes)
|
||||
|
||||
// Get P-256 curve parameters
|
||||
val ecSpec = java.security.spec.ECGenParameterSpec("secp256r1")
|
||||
val params = java.security.AlgorithmParameters.getInstance("EC")
|
||||
params.init(ecSpec)
|
||||
val ecParameterSpec = params.getParameterSpec(java.security.spec.ECParameterSpec::class.java)
|
||||
|
||||
// Create ECPrivateKeySpec with the d value and curve parameters
|
||||
val privKeySpec = java.security.spec.ECPrivateKeySpec(d, ecParameterSpec)
|
||||
|
||||
// Generate the private key
|
||||
val keyFactory = java.security.KeyFactory.getInstance("EC")
|
||||
return keyFactory.generatePrivate(privKeySpec) as java.security.interfaces.ECPrivateKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate PRF (hmac-secret extension) - duplicate from PasskeyAuthenticator
|
||||
*/
|
||||
private fun evaluatePrf(secret: ByteArray, salt: ByteArray): ByteArray {
|
||||
// Step 1: Domain separation - hash salt with "WebAuthn PRF\x00" prefix
|
||||
val prefix = "WebAuthn PRF\u0000".toByteArray(Charsets.UTF_8)
|
||||
val domainSeparatedSalt = prefix + salt
|
||||
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val hashedSalt = md.digest(domainSeparatedSalt)
|
||||
|
||||
// Step 2: Compute HMAC-SHA256(prfSecret, hashedSalt)
|
||||
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
|
||||
val secretKey = javax.crypto.spec.SecretKeySpec(secret, "HmacSHA256")
|
||||
mac.init(secretKey)
|
||||
return mac.doFinal(hashedSalt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ fun VaultStore.getPasskeyByCredentialId(credentialId: ByteArray, db: SQLiteDatab
|
||||
LIMIT 1
|
||||
""".trimIndent()
|
||||
|
||||
val cursor = db.rawQuery(query, arrayOf(credentialIdString))
|
||||
val cursor = db.rawQuery(query, arrayOf(credentialIdString.uppercase()))
|
||||
cursor.use {
|
||||
if (it.moveToFirst()) {
|
||||
return parsePasskeyRow(it)
|
||||
@@ -84,7 +84,7 @@ fun VaultStore.getPasskeysForCredential(credentialId: UUID, db: SQLiteDatabase):
|
||||
""".trimIndent()
|
||||
|
||||
val passkeys = mutableListOf<Passkey>()
|
||||
val cursor = db.rawQuery(query, arrayOf(credentialId.toString()))
|
||||
val cursor = db.rawQuery(query, arrayOf(credentialId.toString().uppercase()))
|
||||
|
||||
cursor.use {
|
||||
while (it.moveToNext()) {
|
||||
@@ -202,8 +202,8 @@ fun VaultStore.insertPasskey(passkey: Passkey, db: SQLiteDatabase) {
|
||||
db.execSQL(
|
||||
insert,
|
||||
arrayOf(
|
||||
passkey.id.toString(),
|
||||
passkey.parentCredentialId.toString(),
|
||||
passkey.id.toString().uppercase(),
|
||||
passkey.parentCredentialId.toString().uppercase(),
|
||||
passkey.rpId,
|
||||
passkey.userHandle,
|
||||
publicKeyString,
|
||||
@@ -243,7 +243,7 @@ fun VaultStore.createCredentialWithPasskey(
|
||||
db.execSQL(
|
||||
serviceInsert,
|
||||
arrayOf(
|
||||
serviceId.toString(),
|
||||
serviceId.toString().uppercase(),
|
||||
displayName,
|
||||
"https://$rpId",
|
||||
logo,
|
||||
@@ -264,7 +264,7 @@ fun VaultStore.createCredentialWithPasskey(
|
||||
db.execSQL(
|
||||
aliasInsert,
|
||||
arrayOf(
|
||||
aliasId.toString(),
|
||||
aliasId.toString().uppercase(),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
@@ -286,9 +286,9 @@ fun VaultStore.createCredentialWithPasskey(
|
||||
db.execSQL(
|
||||
credentialInsert,
|
||||
arrayOf(
|
||||
credentialId.toString(),
|
||||
serviceId.toString(),
|
||||
aliasId.toString(),
|
||||
credentialId.toString().uppercase(),
|
||||
serviceId.toString().uppercase(),
|
||||
aliasId.toString().uppercase(),
|
||||
userName,
|
||||
null,
|
||||
timestamp,
|
||||
@@ -364,7 +364,7 @@ fun VaultStore.replacePasskey(
|
||||
SELECT ServiceId FROM Credentials WHERE Id = ? LIMIT 1
|
||||
""".trimIndent()
|
||||
|
||||
val cursor = db.rawQuery(credQuery, arrayOf(credentialId.toString()))
|
||||
val cursor = db.rawQuery(credQuery, arrayOf(credentialId.toString().uppercase()))
|
||||
cursor.use {
|
||||
if (it.moveToFirst()) {
|
||||
val serviceId = it.getString(0)
|
||||
@@ -388,7 +388,7 @@ fun VaultStore.replacePasskey(
|
||||
WHERE Id = ?
|
||||
""".trimIndent()
|
||||
|
||||
db.execSQL(deleteQuery, arrayOf(timestamp, oldPasskeyId.toString()))
|
||||
db.execSQL(deleteQuery, arrayOf(timestamp, oldPasskeyId.toString().uppercase()))
|
||||
|
||||
// Create the new passkey with the same credential ID
|
||||
val updatedPasskey = newPasskey.copy(
|
||||
@@ -405,7 +405,7 @@ fun VaultStore.replacePasskey(
|
||||
/**
|
||||
* Get a passkey by its ID
|
||||
*/
|
||||
private fun VaultStore.getPasskeyById(passkeyId: UUID, db: SQLiteDatabase): Passkey? {
|
||||
fun VaultStore.getPasskeyById(passkeyId: UUID, db: SQLiteDatabase): Passkey? {
|
||||
val query = """
|
||||
SELECT Id, CredentialId, RpId, UserHandle, PublicKey, PrivateKey, PrfKey,
|
||||
DisplayName, CreatedAt, UpdatedAt, IsDeleted
|
||||
@@ -414,7 +414,10 @@ private fun VaultStore.getPasskeyById(passkeyId: UUID, db: SQLiteDatabase): Pass
|
||||
LIMIT 1
|
||||
""".trimIndent()
|
||||
|
||||
val cursor = db.rawQuery(query, arrayOf(passkeyId.toString()))
|
||||
// Always convert the UUID to uppercase when querying the DB
|
||||
val upperPasskeyId = passkeyId.toString().uppercase()
|
||||
|
||||
val cursor = db.rawQuery(query, arrayOf(upperPasskeyId))
|
||||
cursor.use {
|
||||
if (it.moveToFirst()) {
|
||||
return parsePasskeyRow(it)
|
||||
|
||||
Reference in New Issue
Block a user