fix: Origin parameters and callingAppInfo

This commit is contained in:
J-Jamet
2025-08-25 15:38:34 +02:00
parent bc86ee87a0
commit f2f4c1e63d
5 changed files with 142 additions and 96 deletions

View File

@@ -202,7 +202,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
searchInfo: SearchInfo?
) {
Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(this@PasskeyLauncherActivity, intent) { usageParameters ->
retrievePasskeyUsageRequestParameters(intent, assets) { usageParameters ->
// Save the requested parameters
mUsageParameters = usageParameters
// Manage the passkey to use

View File

@@ -23,13 +23,12 @@ import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import org.json.JSONObject
open class ClientDataNotDefinedResponse(
open class ClientDataBuildResponse(
type: Type,
challenge: ByteArray,
origin: String,
crossOrigin: Boolean? = null,
topOrigin: String? = null,
packageName: String?
): AuthenticatorResponse, ClientDataResponse {
override var clientJson = JSONObject()
@@ -44,9 +43,6 @@ open class ClientDataNotDefinedResponse(
topOrigin?.let {
clientJson.put("topOrigin", it)
}
packageName?.let {
clientJson.put("androidPackageName", packageName)
}
}
override fun json(): JSONObject {

View File

@@ -1,18 +0,0 @@
package com.kunzisoft.keepass.credentialprovider.passkey.util
class AppRelyingPartyRelation {
companion object {
fun isRelationValid(relyingParty: String, apkSigningCertificate: ByteArray?): Boolean {
/*
TODO
to implement this, a request to https://$rp/.well-known/assetlinks.json,
parsing the result and matching the hash of the apkSigningCertificate is needed.
This is needed to make sure that a malicious app can not act as an arbitrary relying party.
In short: prevent phishing
*/
return false
}
}
}

View File

@@ -19,46 +19,91 @@
*/
package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.content.pm.SigningInfo
import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@RequiresApi(Build.VERSION_CODES.P)
class OriginManager(
callingAppInfo: CallingAppInfo?,
assets: AssetManager,
private val relyingParty: String
private val providedClientDataHash: ByteArray?,
private val callingAppInfo: CallingAppInfo?,
private val assets: AssetManager
) {
private val webOrigin: String?
private val apkSigningCertificate: ByteArray? = callingAppInfo?.signingInfo?.apkContentsSigners
?.getOrNull(0)?.toByteArray()
init {
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
// for trusted browsers like Chrome and Firefox
webOrigin = callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/")
}
// TODO isPrivileged app
fun checkPrivilegedApp(
clientDataHash: ByteArray?
fun getOriginAtCreation(
onOriginRetrieved: (origin: String, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (origin: String, signingInfo: SigningInfo) -> Unit
) {
val isPrivilegedApp = webOrigin != null
&& webOrigin == relyingParty && clientDataHash != null
Log.d(TAG, "isPrivilegedApp = $isPrivilegedApp")
if (!isPrivilegedApp) {
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
getOrigin(
onOriginRetrieved = { callOrigin, clientDataHash ->
onOriginRetrieved(callOrigin, clientDataHash)
},
onOriginNotRetrieved = { packageName, signingInfo ->
// Create a new Android Origin and prepare the signature app storage
onOriginCreated(buildAndroidOrigin(packageName), signingInfo)
}
)
}
fun getOriginAtUsage(
storedPackageName: String?,
storedSignature: SigningInfo?,
onOriginRetrieved: (origin: String, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (origin: String) -> Unit
) {
getOrigin(
onOriginRetrieved = { callOrigin, clientDataHash ->
onOriginRetrieved(callOrigin, clientDataHash)
},
onOriginNotRetrieved = { packageName, signingInfo ->
// Verify the app signature to retrieve the origin
// TODO if (packageName == storedPackageName
// && signingInfo == storedSignature) {
onOriginCreated(buildAndroidOrigin(packageName))
//} else {
// throw SecurityException("Android Origin cannot be retrieved, wrong signature")
//}
}
)
}
private fun getOrigin(
onOriginRetrieved: (callOrigin: String, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (packageName: String, signingInfo: SigningInfo) -> Unit
) {
if (callingAppInfo == null) {
throw SecurityException("Calling app info cannot be retrieved")
}
CoroutineScope(Dispatchers.IO).launch {
var callOrigin: String?
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
// for trusted browsers like Chrome and Firefox
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) {
Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo")
onOriginRetrieved(callOrigin, providedClientDataHash)
} else {
onOriginNotRetrieved(callingAppInfo.packageName, callingAppInfo.signingInfo)
}
}
}
}
val origin: String
get() {
return webOrigin ?: relyingParty
}
private fun buildAndroidOrigin(packageName: String): String {
val packageOrigin = "android://${packageName}"
Log.d(TAG, "Origin $packageOrigin retrieved from package name")
return packageOrigin
}
companion object {
private val TAG = OriginManager::class.simpleName

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.AssetManager
import android.os.Build
@@ -43,8 +42,8 @@ import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataNotDefinedResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
@@ -259,11 +258,10 @@ object PasskeyHelper {
assetManager: AssetManager,
passkeyCreated: (Passkey, PublicKeyCredentialCreationParameters) -> Unit
) {
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
val callingAppInfo = getCredentialRequest?.callingAppInfo
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
if (createCredentialRequest == null)
throw CreateCredentialUnknownException("could not retrieve request from intent")
val callingAppInfo = createCredentialRequest.callingAppInfo
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
val relyingParty = creationOptions.relyingPartyEntity.id
@@ -272,9 +270,6 @@ object PasskeyHelper {
val pubKeyCredParams = creationOptions.pubKeyCredParams
val clientDataHash = creationOptions.clientDataHash
val originManager = OriginManager(callingAppInfo, assetManager, relyingParty)
originManager.checkPrivilegedApp(clientDataHash)
val credentialId = KeePassDXRandom.generateCredentialId()
val (keyPair, keyTypeId) = Signature.generateKeyPair(
@@ -282,30 +277,48 @@ object PasskeyHelper {
) ?: throw CreateCredentialUnknownException("no known public key type found")
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
// Create the passkey element
val passkey = Passkey(
username = username,
privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = relyingParty
)
// create new entry in database
passkeyCreated.invoke(
Passkey(
username = username,
privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = relyingParty
),
PublicKeyCredentialCreationParameters(
publicKeyCredentialCreationOptions = creationOptions,
credentialId = credentialId,
signatureKey = Pair(keyPair, keyTypeId),
clientDataResponse = clientDataHash?.let {
ClientDataDefinedResponse(clientDataHash)
} ?: run {
ClientDataNotDefinedResponse(
type = ClientDataNotDefinedResponse.Type.CREATE,
challenge = creationOptions.challenge,
origin = originManager.origin,
packageName = callingAppInfo?.packageName
OriginManager(
providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo,
assets = assetManager
).getOriginAtCreation(
onOriginRetrieved = { origin, clientDataHash ->
passkeyCreated.invoke(
passkey,
PublicKeyCredentialCreationParameters(
publicKeyCredentialCreationOptions = creationOptions,
credentialId = credentialId,
signatureKey = Pair(keyPair, keyTypeId),
clientDataResponse = ClientDataDefinedResponse(clientDataHash)
)
}
)
)
},
onOriginCreated = { origin, signingInfo ->
// TODO store signature
passkeyCreated.invoke(
passkey,
PublicKeyCredentialCreationParameters(
publicKeyCredentialCreationOptions = creationOptions,
credentialId = credentialId,
signatureKey = Pair(keyPair, keyTypeId),
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.CREATE,
challenge = creationOptions.challenge,
origin = origin
)
)
)
}
)
}
@@ -340,8 +353,8 @@ object PasskeyHelper {
}
fun retrievePasskeyUsageRequestParameters(
context: Context,
intent: Intent,
assetManager: AssetManager,
result: (PublicKeyCredentialUsageParameters) -> Unit
) {
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
@@ -354,23 +367,33 @@ object PasskeyHelper {
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
val relyingParty = requestOptions.rpId
val originManager = OriginManager(callingAppInfo, context.assets, relyingParty)
originManager.checkPrivilegedApp(clientDataHash)
result.invoke(
PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = clientDataHash?.let {
ClientDataDefinedResponse(clientDataHash)
} ?: run {
ClientDataNotDefinedResponse(
type = ClientDataNotDefinedResponse.Type.GET,
challenge = requestOptions.challenge,
origin = originManager.origin,
packageName = callingAppInfo.packageName
OriginManager(
providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo,
assets = assetManager
).getOriginAtUsage(
storedPackageName = null, // TODO Retrieved package name and signature
storedSignature = null,
onOriginRetrieved = { origin, clientDataHash ->
result.invoke(
PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = ClientDataDefinedResponse(clientDataHash)
)
}
)
)
},
onOriginCreated = { origin ->
result.invoke(
PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = requestOptions.challenge,
origin = origin
)
)
)
}
)
}