diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index 8a28cd30e..0be66c40e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -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 diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataNotDefinedResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt similarity index 92% rename from app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataNotDefinedResponse.kt rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt index d7b9bc4f3..6b1453fc3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataNotDefinedResponse.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt @@ -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 { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/AppRelyingPartyRelation.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/AppRelyingPartyRelation.kt deleted file mode 100644 index a4a5e4f49..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/AppRelyingPartyRelation.kt +++ /dev/null @@ -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 - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt index e8700fba1..2d02c041c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt @@ -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 diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index 6661451a8..12ede4be2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -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 + ) + ) + ) + } ) }