From 4f10d13691e1423aae7c4b83261865c2e570ca33 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Fri, 29 Aug 2025 12:23:44 +0200 Subject: [PATCH] fix: Small refactoring and add doc --- .../activity/PasskeyLauncherActivity.kt | 57 ++++--- .../passkey/data/ClientDataBuildResponse.kt | 2 +- .../passkey/util/OriginManager.kt | 24 ++- .../passkey/util/PasskeyHelper.kt | 143 +++++++++++++++--- 4 files changed, 172 insertions(+), 54 deletions(-) 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 28a75ff90..75fdce993 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 @@ -41,7 +41,6 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters -import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.checkInAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId @@ -49,6 +48,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSe import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedClientDataResponse import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin @@ -92,35 +92,25 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { passkey?.let { mUsageParameters?.let { usageParameters -> // Check verified origin - if (usageParameters.androidAppVerified) { - PendingIntentHandler.setGetCredentialResponse( - responseIntent, - GetCredentialResponse( - buildPasskeyPublicKeyCredential( - usageParameters = usageParameters, - passkey = passkey - ) - ) - ) - } else { - usageParameters.androidApp.checkInAppOrigin( - appOrigin = appOrigin, - onOriginChecked = { - PendingIntentHandler.setGetCredentialResponse( - responseIntent, - GetCredentialResponse( - buildPasskeyPublicKeyCredential( - usageParameters = usageParameters, - passkey = passkey - ) + getVerifiedClientDataResponse( + usageParameters = usageParameters, + appOrigin = appOrigin, + onOriginChecked = { clientDataResponse -> + PendingIntentHandler.setGetCredentialResponse( + responseIntent, + GetCredentialResponse( + buildPasskeyPublicKeyCredential( + requestOptions = usageParameters.publicKeyCredentialRequestOptions, + clientDataResponse = clientDataResponse, + passkey = passkey ) ) - }, - onOriginNotChecked = { - throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}") - } - ) - } + ) + }, + onOriginNotChecked = { + throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}") + } + ) } ?: run { throw IOException("Usage parameters is null") } @@ -215,14 +205,15 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { ?.getEntryById(NodeIdUUID(nodeId)) ?.getEntryInfo(database) ?.passkey - ?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found") + ?: throw GetCredentialUnknownException("No passkey with nodeId $nodeId found") val result = Intent() PendingIntentHandler.setGetCredentialResponse( result, GetCredentialResponse( buildPasskeyPublicKeyCredential( - usageParameters = usageParameters, + requestOptions = usageParameters.publicKeyCredentialRequestOptions, + clientDataResponse = usageParameters.clientDataResponse, passkey = passkey ) ) @@ -243,7 +234,11 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { appOrigin: AppOrigin? ) { Log.d(TAG, "Launch passkey selection") - retrievePasskeyUsageRequestParameters(intent, assets, appOrigin) { usageParameters -> + retrievePasskeyUsageRequestParameters( + intent = intent, + assetManager = assets, + appOrigin = appOrigin + ) { 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/ClientDataBuildResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt index 6b1453fc3..8427b7b68 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt @@ -27,7 +27,7 @@ open class ClientDataBuildResponse( type: Type, challenge: ByteArray, origin: String, - crossOrigin: Boolean? = null, + crossOrigin: Boolean? = false, topOrigin: String? = null, ): AuthenticatorResponse, ClientDataResponse { override var clientJson = JSONObject() 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 d396af50f..98d6bd93a 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 @@ -30,6 +30,9 @@ import com.kunzisoft.keepass.model.AppOrigin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +/** + * Utility class to manage the origin of credential provider applications + */ @RequiresApi(Build.VERSION_CODES.P) class OriginManager( private val providedClientDataHash: ByteArray?, @@ -37,6 +40,11 @@ class OriginManager( private val assets: AssetManager ) { + /** + * Retrieves the Android origin to be stored in the database, + * call [onOriginRetrieved] if the origin is already calculated by the system + * call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database + */ suspend fun getOriginAtCreation( onOriginRetrieved: (appInfoToStore: AppOrigin, clientDataHash: ByteArray) -> Unit, onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit @@ -62,9 +70,9 @@ class OriginManager( } /** - * Retrieve the Android origin from an [AppOrigin], - * call [onOriginRetrieved] if the origin is already calculated by the system - * call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database + * Retrieves the origin to verify usage, + * calls [onOriginRetrieved] if the origin is already calculated by the system + * calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database */ suspend fun getOriginAtUsage( appOrigin: AppOrigin?, @@ -91,6 +99,12 @@ class OriginManager( ) } + /** + * Utility method to retrieve the origin asynchronously, + * checks for the presence of the application in the privilege list of the trustedPackages.json file, + * call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list + * call [onOriginNotRetrieved] if the origin is not retrieved from the system + */ private suspend fun getOrigin( onOriginRetrieved: (appInfoRetrieved: AppIdentifier, origin: String, clientDataHash: ByteArray) -> Unit, onOriginNotRetrieved: (appInfoRetrieved: AppIdentifier) -> Unit @@ -113,9 +127,9 @@ class OriginManager( withContext(Dispatchers.Main) { if (callOrigin != null && providedClientDataHash != null) { Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo") - onOriginRetrieved(appIdentifier, callOrigin, providedClientDataHash) + onOriginRetrieved(appIdentifier, callOrigin, providedClientDataHash) } else { - onOriginNotRetrieved(appIdentifier) + onOriginNotRetrieved(appIdentifier) } } } 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 3b2a614fa..01be9c148 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 @@ -44,12 +44,14 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttest 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.ClientDataResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.checkInAppOrigin import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.Passkey @@ -65,6 +67,11 @@ import javax.crypto.KeyGenerator import javax.crypto.Mac import javax.crypto.SecretKey +/** + * Utility class to manage the passkey elements, + * allows to add and retrieve intent values with preconfigured keys, + * and makes it easy to create creation and usage requests + */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) object PasskeyHelper { @@ -119,6 +126,9 @@ object PasskeyHelper { } } + /** + * Add an authentication code generated by an entry to the intent + */ fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) { putExtras(Bundle().apply { val timestamp = Instant.now().epochSecond @@ -132,48 +142,78 @@ object PasskeyHelper { }) } + /** + * Retrieve the passkey from the intent + */ fun Intent.retrievePasskey(): Passkey? { return this.getParcelableExtraCompat(EXTRA_PASSKEY) } + /** + * Remove the passkey from the intent + */ fun Intent.removePasskey() { return this.removeExtra(EXTRA_PASSKEY) } + /** + * Add the search info to the intent + */ fun Intent.addSearchInfo(searchInfo: SearchInfo?) { searchInfo?.let { putExtra(EXTRA_SEARCH_INFO, searchInfo) } } + /** + * Retrieve the search info from the intent + */ fun Intent.retrieveSearchInfo(): SearchInfo? { return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO) } + /** + * Add the app origin to the intent + */ fun Intent.addAppOrigin(appOrigin: AppOrigin?) { appOrigin?.let { putExtra(EXTRA_APP_ORIGIN, appOrigin) } } + /** + * Retrieve the app origin from the intent + */ fun Intent.retrieveAppOrigin(): AppOrigin? { return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN) } + /** + * Remove the app origin from the intent + */ fun Intent.removeAppOrigin() { return this.removeExtra(EXTRA_APP_ORIGIN) } + /** + * Add the node id to the intent, useful for auto passkey selection + */ fun Intent.addNodeId(nodeId: UUID?) { nodeId?.let { putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId)) } } + /** + * Retrieve the node id from the intent + */ fun Intent.retrieveNodeId(): UUID? { return getParcelableExtraCompat(EXTRA_NODE_ID)?.uuid } + /** + * Check the timestamp and authentication code transmitted via PendingIntent + */ fun checkSecurity(intent: Intent, nodeId: UUID?) { val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP) if (timestampString.isNullOrEmpty()) @@ -192,12 +232,9 @@ object PasskeyHelper { ) } - private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray { - return generateAuthenticationCode( - (nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString() - ) - } - + /** + * Verify the authentication code from the encrypted message received from the intent + */ private fun verifyAuthenticationCode( valueToCheck: String?, authenticationCode: ByteArray @@ -210,6 +247,18 @@ object PasskeyHelper { throw CreateCredentialUnknownException("Authentication code incorrect") } + /** + * Generate the authentication code base on the entry [nodeId] and [timestamp] + */ + private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray { + return generateAuthenticationCode( + (nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString() + ) + } + + /** + * Generate the authentication code base on the entry [message] + */ private fun generateAuthenticationCode(message: String): ByteArray { val keyStore = KeyStore.getInstance(KEYSTORE_TYPE) keyStore.load(null) @@ -226,6 +275,9 @@ object PasskeyHelper { return authenticationCode } + /** + * Generate the HMAC key if cannot be found in the KeyStore + */ private fun generateKey(): SecretKey? { val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE @@ -240,6 +292,9 @@ object PasskeyHelper { return key } + /** + * Retrieve the [PublicKeyCredentialCreationOptions] from the intent + */ fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions { val request = this if (request.callingRequest !is CreatePublicKeyCredentialRequest) { @@ -252,6 +307,9 @@ object PasskeyHelper { ) } + /** + * Retrieve the [GetPublicKeyCredentialOption] from the intent + */ fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption { val request = this if (request.credentialOptions.size != 1) { @@ -263,6 +321,12 @@ object PasskeyHelper { return request.credentialOptions[0] as GetPublicKeyCredentialOption } + /** + * Utility method to create a passkey and the associated creation request parameters + * [intent] allows to retrieve the request + * [assetManager] has been transferred to the origin manager to manage package verification files + * [passkeyCreated] is called asynchronously when the passkey has been created + */ suspend fun retrievePasskeyCreationRequestParameters( intent: Intent, assetManager: AssetManager, @@ -325,8 +389,7 @@ object PasskeyHelper { clientDataResponse = ClientDataBuildResponse( type = ClientDataBuildResponse.Type.CREATE, challenge = creationOptions.challenge, - origin = origin, - crossOrigin = false // TODO should always be false? + origin = origin ) ) ) @@ -334,6 +397,10 @@ object PasskeyHelper { ) } + /** + * Build the passkey public key credential response, + * by calling this method the user is always recognized as present and verified + */ fun buildCreatePublicKeyCredentialResponse( publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters ): CreatePublicKeyCredentialResponse { @@ -351,8 +418,8 @@ object PasskeyHelper { ) ?: mapOf()), userPresent = true, userVerified = true, - backupEligibility = false, // TODO should always be false? - backupState = false, // TODO should always be false? + backupEligibility = false, + backupState = false, publicKeyTypeId = keyTypeId, publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!, clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse @@ -364,6 +431,13 @@ object PasskeyHelper { return CreatePublicKeyCredentialResponse(responseJson) } + /** + * Utility method to use a passkey and create the associated usage request parameters + * [intent] allows to retrieve the request + * [assetManager] has been transferred to the origin manager to manage package verification files + * [appOrigin] retrieves the origin params stored in an entry, which may be null if not found or if the database is closed. + * [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified + */ suspend fun retrievePasskeyUsageRequestParameters( intent: Intent, assetManager: AssetManager, @@ -402,8 +476,7 @@ object PasskeyHelper { clientDataResponse = ClientDataBuildResponse( type = ClientDataBuildResponse.Type.GET, challenge = requestOptions.challenge, - origin = origin, - crossOrigin = false // TODO should always be false? + origin = origin ), androidApp = appIdentifier, androidAppVerified = verified @@ -413,21 +486,26 @@ object PasskeyHelper { ) } + /** + * Build the passkey public key credential response, + * by calling this method the user is always recognized as present and verified + */ fun buildPasskeyPublicKeyCredential( - usageParameters: PublicKeyCredentialUsageParameters, + requestOptions: PublicKeyCredentialRequestOptions, + clientDataResponse: ClientDataResponse, passkey: Passkey ): PublicKeyCredential { val getCredentialResponse = FidoPublicKeyCredential( id = passkey.credentialId, response = AuthenticatorAssertionResponse( - requestOptions = usageParameters.publicKeyCredentialRequestOptions, + requestOptions = requestOptions, userPresent = true, userVerified = true, - backupEligibility = false, // TODO should always be false? - backupState = false, // TODO should always be false? + backupEligibility = false, + backupState = false, userHandle = passkey.userHandle, privateKey = passkey.privateKeyPem, - clientDataResponse = usageParameters.clientDataResponse + clientDataResponse = clientDataResponse ), authenticatorAttachment = "platform" ).json() @@ -435,4 +513,35 @@ object PasskeyHelper { return PublicKeyCredential(getCredentialResponse) } + + /** + * Verify that the application signature is contained in the [appOrigin] + * or that the webDomain contains the origin + */ + fun getVerifiedClientDataResponse( + usageParameters: PublicKeyCredentialUsageParameters, + appOrigin: AppOrigin?, + onOriginChecked: (clientDataResponse: ClientDataResponse) -> Unit, + onOriginNotChecked: () -> Unit + ) { + if (usageParameters.androidAppVerified) { + onOriginChecked(usageParameters.clientDataResponse) + } else { + usageParameters.androidApp.checkInAppOrigin( + appOrigin = appOrigin, + onOriginChecked = { origin -> + // Origin checked by Android app signature + onOriginChecked( + ClientDataBuildResponse( + type = ClientDataBuildResponse.Type.GET, + challenge = usageParameters.publicKeyCredentialRequestOptions.challenge, + origin = origin + ) + ) + }, + onOriginNotChecked + ) + } + } + } \ No newline at end of file