mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Origin parameters and callingAppInfo
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user