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 276a45957..c78511b5b 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 @@ -23,7 +23,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build -import android.os.Bundle import android.util.Log import android.widget.Toast import androidx.activity.result.ActivityResultLauncher @@ -42,16 +41,18 @@ 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 -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addOriginAppInfo import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo 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.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey +import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveOriginAppInfo import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters @@ -59,13 +60,13 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retri import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.helper.SearchHelper -import com.kunzisoft.keepass.model.OriginApp +import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import java.io.IOException import java.io.InvalidObjectException import java.util.UUID @@ -75,34 +76,49 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { private var mUsageParameters: PublicKeyCredentialUsageParameters? = null private var mCreationParameters: PublicKeyCredentialCreationParameters? = null private var mPasskey: Passkey? = null - private var mSearchInfo: SearchInfo = SearchInfo() - private var mOriginApp: OriginApp = OriginApp() private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher? = this.buildActivityResultLauncher( lockDatabase = true, dataTransformation = { intent -> - Log.d(TAG, "Passkey selection result") - val passkey = intent?.retrievePasskey() - intent?.removePasskey() // Build a new formatted response from the selection response val responseIntent = Intent() - passkey?.let { - mUsageParameters?.let { usageParameters -> - PendingIntentHandler.setGetCredentialResponse( - responseIntent, - GetCredentialResponse( - buildPasskeyPublicKeyCredential( - usageParameters = usageParameters, - passkey = passkey - ) + try { + Log.d(TAG, "Passkey selection result") + val passkey = intent?.retrievePasskey() + val appOrigin = intent?.retrieveAppOrigin() + intent?.removePasskey() + intent?.removeAppOrigin() + passkey?.let { + mUsageParameters?.let { usageParameters -> + // Check verified origin + usageParameters.androidApp.checkInAppOrigin( + appOrigin = appOrigin, + onOriginChecked = { + usageParameters.androidAppVerified = true + PendingIntentHandler.setGetCredentialResponse( + responseIntent, + GetCredentialResponse( + buildPasskeyPublicKeyCredential( + usageParameters = usageParameters, + passkey = passkey + ) + ) + ) + }, + onOriginNotChecked = { + throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}") + } ) - ) + } ?: run { + throw IOException("Usage parameters is null") + } } ?: run { - Log.e(TAG, "Unable to return passkey, usage parameters are empty") + throw IOException("Passkey is null") } - } ?: run { - Log.e(TAG, "Unable to get the passkey for response") + } catch (e: Exception) { + Log.e(TAG, "Unable to create selection response for passkey", e) + showError(e) } // Return the response responseIntent @@ -113,21 +129,29 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { this.buildActivityResultLauncher( lockDatabase = true, dataTransformation = { intent -> - Log.d(TAG, "Passkey registration result") - val passkey = intent?.retrievePasskey() - intent?.removePasskey() // Build a new formatted response from the creation response val responseIntent = Intent() - // If registered passkey is the same as the one we want to validate, - if (mPasskey == passkey) { - mCreationParameters?.let { - PendingIntentHandler.setCreateCredentialResponse( - intent = responseIntent, - response = buildCreatePublicKeyCredentialResponse( - publicKeyCredentialCreationParameters = it + try { + Log.d(TAG, "Passkey registration result") + val passkey = intent?.retrievePasskey() + intent?.removePasskey() + intent?.removeAppOrigin() + // If registered passkey is the same as the one we want to validate, + if (mPasskey == passkey) { + mCreationParameters?.let { + PendingIntentHandler.setCreateCredentialResponse( + intent = responseIntent, + response = buildCreatePublicKeyCredentialResponse( + publicKeyCredentialCreationParameters = it + ) ) - ) + } + } else { + throw SecurityException("Passkey was modified before registration") } + } catch (e: Exception) { + Log.e(TAG, "Unable to create registration response for passkey", e) + showError(e) } responseIntent } @@ -141,31 +165,27 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { return false } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - mSearchInfo = intent.retrieveSearchInfo() ?: mSearchInfo - mOriginApp = intent.retrieveOriginAppInfo() ?: mOriginApp - } - override fun onDatabaseRetrieved(database: ContextualDatabase?) { super.onDatabaseRetrieved(database) lifecycleScope.launch(CoroutineExceptionHandler { _, e -> Log.e(TAG, "Passkey launch error", e) - Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show() + showError(e) setResult(RESULT_CANCELED) finish() }) { + val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo() + val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin() val nodeId = intent.retrieveNodeId() checkSecurity(intent, nodeId) when (mSpecialMode) { SpecialMode.SELECTION -> { - launchSelection(database, nodeId, mSearchInfo, mOriginApp) + launchSelection(database, nodeId, searchInfo, appOrigin) } SpecialMode.REGISTRATION -> { // TODO Registration in predefined group // launchRegistration(database, nodeId, mSearchInfo) - launchRegistration(database, null, mSearchInfo) + launchRegistration(database, null, searchInfo) } else -> { throw InvalidObjectException("Passkey launch mode not supported") @@ -209,14 +229,17 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { database: ContextualDatabase?, nodeId: UUID?, searchInfo: SearchInfo?, - originApp: OriginApp? + appOrigin: AppOrigin? ) { Log.d(TAG, "Launch passkey selection") - retrievePasskeyUsageRequestParameters(intent, assets, originApp) { usageParameters -> + retrievePasskeyUsageRequestParameters(intent, assets, appOrigin) { usageParameters -> // Save the requested parameters mUsageParameters = usageParameters // Manage the passkey to use nodeId?.let { nodeId -> + if (usageParameters.androidAppVerified.not()) { + throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}") + } autoSelectPasskeyAndSetResult(database, nodeId) } ?: run { SearchHelper.checkAutoSearchInfo( @@ -290,7 +313,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { val registerInfo = RegisterInfo( searchInfo = searchInfo, passkey = passkey, - originApp = appInfoToStore + appOrigin = appInfoToStore ) // If nodeId already provided nodeId?.let { nodeId -> @@ -336,6 +359,10 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { ) } + private fun showError(e: Throwable) { + Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show() + } + companion object { private val TAG = PasskeyLauncherActivity::class.java.name @@ -352,7 +379,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { context: Context, specialMode: SpecialMode, searchInfo: SearchInfo? = null, - originApp: OriginApp? = null, + appOrigin: AppOrigin? = null, nodeId: UUID? = null ): PendingIntent? { return PendingIntent.getActivity( @@ -362,7 +389,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { addSpecialMode(specialMode) addTypeMode(TypeMode.PASSKEY) addSearchInfo(searchInfo) - addOriginAppInfo(originApp) + addAppOrigin(appOrigin) addNodeId(nodeId) addAuthCode(nodeId) }, diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt index f7f38627a..6f6bcb652 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt @@ -144,7 +144,7 @@ class PasskeyProviderService : CredentialProviderService() { context = applicationContext, specialMode = SpecialMode.SELECTION, nodeId = passkeyEntry.id, - originApp = passkeyEntry.originApp + appOrigin = passkeyEntry.appOrigin )?.let { usagePendingIntent -> val passkey = passkeyEntry.passkey passkeyEntries.add( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt index 16b01a345..40367990a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt @@ -19,7 +19,11 @@ */ package com.kunzisoft.keepass.credentialprovider.passkey.data +import com.kunzisoft.keepass.model.AppIdentifier + data class PublicKeyCredentialUsageParameters( val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions, - val clientDataResponse: ClientDataResponse + val clientDataResponse: ClientDataResponse, + val androidApp: AppIdentifier, + var androidAppVerified: Boolean ) \ 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 b05db330b..4cc320d82 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 @@ -25,7 +25,8 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.credentials.provider.CallingAppInfo import com.kunzisoft.encrypt.HashManager.getApplicationSignatures -import com.kunzisoft.keepass.model.OriginApp +import com.kunzisoft.keepass.model.AppIdentifier +import com.kunzisoft.keepass.model.AppOrigin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -37,47 +38,62 @@ class OriginManager( ) { suspend fun getOriginAtCreation( - onOriginRetrieved: (appInfoToStore: OriginApp, clientDataHash: ByteArray) -> Unit, - onOriginCreated: (appInfoToStore: OriginApp, origin: String) -> Unit + onOriginRetrieved: (appInfoToStore: AppOrigin, clientDataHash: ByteArray) -> Unit, + onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit ) { getOrigin( - onOriginRetrieved = { callOrigin, clientDataHash -> - onOriginRetrieved(OriginApp(webDomain = callOrigin), clientDataHash) + onOriginRetrieved = { appIdentifier, callOrigin, clientDataHash -> + onOriginRetrieved( + AppOrigin().apply { + addIdentifier(appIdentifier) + addWebDomain(callOrigin) + }, + clientDataHash + ) }, - onOriginNotRetrieved = { storeAppInfo -> + onOriginNotRetrieved = { appIdentifier -> // Create a new Android Origin and prepare the signature app storage onOriginCreated( - storeAppInfo, - buildAndroidOrigin(storeAppInfo.appId) + AppOrigin().apply { addIdentifier(appIdentifier) }, + appIdentifier.buildAndroidOrigin() ) } ) } + /** + * 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 + */ suspend fun getOriginAtUsage( - appInfoStored: OriginApp?, - onOriginRetrieved: (clientDataHash: ByteArray) -> Unit, - onOriginCreated: (origin: String) -> Unit + appOrigin: AppOrigin?, + onOriginRetrieved: (appIdentifier: AppIdentifier, clientDataHash: ByteArray) -> Unit, + onOriginCreated: (appIdentifier: AppIdentifier, origin: String, originVerified: Boolean) -> Unit ) { getOrigin( - onOriginRetrieved = { origin, clientDataHash -> - onOriginRetrieved(clientDataHash) + onOriginRetrieved = { appIdentifier, origin, clientDataHash -> + onOriginRetrieved(appIdentifier, clientDataHash) }, - onOriginNotRetrieved = { appInfoCalled -> + onOriginNotRetrieved = { appIdentifierToCheck -> // Verify the app signature to retrieve the origin - if (appInfoCalled.appId == appInfoStored?.appId - && appInfoCalled.appSignature == appInfoStored?.appSignature) { - onOriginCreated(buildAndroidOrigin(appInfoCalled.appId)) - } else { - throw SecurityException("Wrong signature for ${appInfoCalled.appId}, ${appInfoCalled.appSignature} retrieved but ${appInfoStored?.appSignature} expected") - } + val androidOrigin = appIdentifierToCheck.buildAndroidOrigin() + appIdentifierToCheck.checkInAppOrigin( + appOrigin = appOrigin, + onOriginChecked = { + onOriginCreated(appIdentifierToCheck, androidOrigin, true) + }, + onOriginNotChecked = { + onOriginCreated(appIdentifierToCheck, androidOrigin, false) + } + ) } ) } private suspend fun getOrigin( - onOriginRetrieved: (origin: String, clientDataHash: ByteArray) -> Unit, - onOriginNotRetrieved: (appInfoRetrieved: OriginApp) -> Unit + onOriginRetrieved: (appInfoRetrieved: AppIdentifier, origin: String, clientDataHash: ByteArray) -> Unit, + onOriginNotRetrieved: (appInfoRetrieved: AppIdentifier) -> Unit ) { if (callingAppInfo == null) { throw SecurityException("Calling app info cannot be retrieved") @@ -89,34 +105,60 @@ class OriginManager( } // for trusted browsers like Chrome and Firefox callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/") + val appIdentifier = AppIdentifier( + id = callingAppInfo.packageName, + signature = callingAppInfo.signingInfo + .getApplicationSignatures() + ) withContext(Dispatchers.Main) { if (callOrigin != null && providedClientDataHash != null) { Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo") - onOriginRetrieved(callOrigin, providedClientDataHash) + onOriginRetrieved(appIdentifier, callOrigin, providedClientDataHash) } else { - onOriginNotRetrieved( - OriginApp( - appId = callingAppInfo.packageName, - appSignature = callingAppInfo.signingInfo.getApplicationSignatures() - ) - ) + onOriginNotRetrieved(appIdentifier) } } } } - /** - * Builds an Android Origin from a package name. - */ - private fun buildAndroidOrigin(packageName: String?): String { - if (packageName.isNullOrEmpty()) - throw SecurityException("Package name cannot be empty") - val packageOrigin = "androidapp://${packageName}" - Log.d(TAG, "Origin $packageOrigin retrieved from package name") - return packageOrigin - } - companion object { private val TAG = OriginManager::class.simpleName + + /** + * Verify that the application signature is contained in the [appOrigin] + */ + fun AppIdentifier.checkInAppOrigin( + appOrigin: AppOrigin?, + onOriginChecked: (origin: String) -> Unit, + onOriginNotChecked: () -> Unit + ) { + // Verify the app signature to retrieve the origin + val appIdentifierStored = appOrigin?.appIdentifiers?.filter { + it.id == this.id + } + if (appIdentifierStored?.any { it.signature == this.signature } == true) { + onOriginChecked(this.buildAndroidOrigin()) + } else { + onOriginNotChecked() + } + } + + /** + * Builds an Android Origin from a AppIdentifier + */ + fun AppIdentifier.buildAndroidOrigin(): String { + return buildAndroidOrigin(this.id) + } + + /** + * Builds an Android Origin from a package name. + */ + private fun buildAndroidOrigin(packageName: String?): String { + if (packageName.isNullOrEmpty()) + throw SecurityException("Package name cannot be empty") + val packageOrigin = "androidapp://${packageName}" + Log.d(TAG, "Origin $packageOrigin retrieved from package name") + return packageOrigin + } } } \ No newline at end of file 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 c132d3848..3b2a614fa 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 @@ -50,8 +50,8 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential 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.model.AppOrigin import com.kunzisoft.keepass.model.EntryInfo -import com.kunzisoft.keepass.model.OriginApp import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.utils.StringUtil.toHexString @@ -68,13 +68,13 @@ import javax.crypto.SecretKey @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) object PasskeyHelper { - private const val EXTRA_PASSKEY_ELEMENT = "com.kunzisoft.keepass.passkey.extra.EXTRA_PASSKEY_ELEMENT" + private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey" private const val HMAC_TYPE = "HmacSHA256" - private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO" - private const val EXTRA_ORIGIN_APP_INFO = "com.kunzisoft.keepass.extra.ORIGIN_INFO" + private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo" + private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin" private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" @@ -103,7 +103,8 @@ object PasskeyHelper { entryInfo.passkey?.let { val mReplyIntent = Intent() Log.d(javaClass.name, "Success Passkey manual selection") - mReplyIntent.putExtra(EXTRA_PASSKEY_ELEMENT, entryInfo.passkey) + mReplyIntent.putExtra(EXTRA_PASSKEY, entryInfo.passkey) + mReplyIntent.putExtra(EXTRA_APP_ORIGIN, entryInfo.appOrigin) extras?.let { mReplyIntent.putExtras(it) } @@ -132,11 +133,11 @@ object PasskeyHelper { } fun Intent.retrievePasskey(): Passkey? { - return this.getParcelableExtraCompat(EXTRA_PASSKEY_ELEMENT) + return this.getParcelableExtraCompat(EXTRA_PASSKEY) } fun Intent.removePasskey() { - return this.removeExtra(EXTRA_PASSKEY_ELEMENT) + return this.removeExtra(EXTRA_PASSKEY) } fun Intent.addSearchInfo(searchInfo: SearchInfo?) { @@ -149,14 +150,18 @@ object PasskeyHelper { return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO) } - fun Intent.addOriginAppInfo(originApp: OriginApp?) { - originApp?.let { - putExtra(EXTRA_ORIGIN_APP_INFO, originApp) + fun Intent.addAppOrigin(appOrigin: AppOrigin?) { + appOrigin?.let { + putExtra(EXTRA_APP_ORIGIN, appOrigin) } } - fun Intent.retrieveOriginAppInfo(): OriginApp? { - return this.getParcelableExtraCompat(EXTRA_ORIGIN_APP_INFO) + fun Intent.retrieveAppOrigin(): AppOrigin? { + return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN) + } + + fun Intent.removeAppOrigin() { + return this.removeExtra(EXTRA_APP_ORIGIN) } fun Intent.addNodeId(nodeId: UUID?) { @@ -261,7 +266,7 @@ object PasskeyHelper { suspend fun retrievePasskeyCreationRequestParameters( intent: Intent, assetManager: AssetManager, - passkeyCreated: (Passkey, OriginApp?, PublicKeyCredentialCreationParameters) -> Unit + passkeyCreated: (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit ) { val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) if (createCredentialRequest == null) @@ -362,7 +367,7 @@ object PasskeyHelper { suspend fun retrievePasskeyUsageRequestParameters( intent: Intent, assetManager: AssetManager, - originApp: OriginApp?, + appOrigin: AppOrigin?, result: (PublicKeyCredentialUsageParameters) -> Unit ) { val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) @@ -379,16 +384,18 @@ object PasskeyHelper { callingAppInfo = callingAppInfo, assets = assetManager ).getOriginAtUsage( - appInfoStored = originApp, - onOriginRetrieved = { clientDataHash -> + appOrigin = appOrigin, + onOriginRetrieved = { appIdentifier, clientDataHash -> result.invoke( PublicKeyCredentialUsageParameters( publicKeyCredentialRequestOptions = requestOptions, - clientDataResponse = ClientDataDefinedResponse(clientDataHash) + clientDataResponse = ClientDataDefinedResponse(clientDataHash), + androidApp = appIdentifier, + androidAppVerified = true ) ) }, - onOriginCreated = { origin -> + onOriginCreated = { appIdentifier, origin, verified -> result.invoke( PublicKeyCredentialUsageParameters( publicKeyCredentialRequestOptions = requestOptions, @@ -397,7 +404,9 @@ object PasskeyHelper { challenge = requestOptions.challenge, origin = origin, crossOrigin = false // TODO should always be false? - ) + ), + androidApp = appIdentifier, + androidAppVerified = verified ) ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt index b4dc6e242..53436b31b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt @@ -534,10 +534,15 @@ abstract class TemplateAbstractView< } protected fun getCustomField(fieldName: String): Field { + return getCustomFieldOrNull(fieldName) + ?: Field(fieldName, ProtectedString(false)) + } + + protected fun getCustomFieldOrNull(fieldName: String): Field? { return getCustomField(fieldName, templateFieldNotEmpty = false, retrieveDefaultValues = false - ) ?: Field(fieldName, ProtectedString(false)) + ) } private fun getCustomField(fieldName: String, diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt index 9b90d9fb5..0350c24b0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt @@ -20,7 +20,7 @@ import com.kunzisoft.keepass.database.helper.getLocalizedName import com.kunzisoft.keepass.database.helper.isStandardPasswordName import com.kunzisoft.keepass.model.DataDate import com.kunzisoft.keepass.model.DataTime -import com.kunzisoft.keepass.model.OriginAppEntryField +import com.kunzisoft.keepass.model.AppOriginEntryField import com.kunzisoft.keepass.model.PasskeyEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields @@ -260,15 +260,12 @@ class TemplateEditView @JvmOverloads constructor(context: Context, override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean, retrieveDefaultValues: Boolean) { super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues) - mEntryInfo?.otpModel = OtpEntryFields.parseFields { key -> - getCustomField(key).protectedValue.toString() - }?.otpModel - mEntryInfo?.passkey = PasskeyEntryFields.parseFields { key -> - getCustomField(key).protectedValue.toString() - } - mEntryInfo?.originApp = OriginAppEntryField.parseFields { key -> - getCustomField(key).protectedValue.toString() + val getField: (id: String) -> String? = { key -> + getCustomFieldOrNull(key)?.protectedValue?.stringValue } + mEntryInfo?.otpModel = OtpEntryFields.parseFields(getField)?.otpModel + mEntryInfo?.passkey = PasskeyEntryFields.parseFields(getField) + mEntryInfo?.appOrigin = AppOriginEntryField.parseFields(getField) } override fun onRestoreEntryInstanceState(state: SavedState) { diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt index d01fd260e..430da31ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt @@ -36,7 +36,7 @@ import androidx.core.text.util.LinkifyCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.model.OriginAppEntryField.APPLICATION_ID_FIELD_NAME +import com.kunzisoft.keepass.model.AppOriginEntryField.APPLICATION_ID_FIELD_NAME import com.kunzisoft.keepass.utils.UriUtil.openExternalApp diff --git a/database/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt b/database/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt index a576229e7..f0efab2f2 100644 --- a/database/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt +++ b/database/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt @@ -33,9 +33,9 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.model.AppOrigin +import com.kunzisoft.keepass.model.AppOriginEntryField import com.kunzisoft.keepass.model.EntryInfo -import com.kunzisoft.keepass.model.OriginApp -import com.kunzisoft.keepass.model.OriginAppEntryField import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.PasskeyEntryFields import com.kunzisoft.keepass.otp.OtpElement @@ -367,9 +367,9 @@ class Entry : Node, EntryVersionedInterface { return null } - fun getOriginApp(): OriginApp? { + fun getAppOrigin(): AppOrigin? { entryKDBX?.let { - return OriginAppEntryField.parseFields { key -> + return AppOriginEntryField.parseFields { key -> it.getFieldValue(key)?.toString() } } @@ -494,7 +494,7 @@ class Entry : Node, EntryVersionedInterface { entryInfo.otpModel = getOtpElement()?.otpModel // Add Passkey entryInfo.passkey = getPasskey() - entryInfo.originApp = getOriginApp() + entryInfo.appOrigin = getAppOrigin() if (!raw) { // Replace parameter fields by generated OTP fields entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) diff --git a/database/src/main/java/com/kunzisoft/keepass/model/OriginApp.kt b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt similarity index 50% rename from database/src/main/java/com/kunzisoft/keepass/model/OriginApp.kt rename to database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt index c765de756..58c35849e 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/OriginApp.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt @@ -23,8 +23,39 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class OriginApp( - val appId: String? = null, - val appSignature: String? = null, - val webDomain: String? = null +data class AppOrigin( + val appIdentifiers: MutableList = mutableListOf(), + val webDomains: MutableList = mutableListOf() +) : Parcelable { + + fun addIdentifier(appIdentifier: AppIdentifier) { + appIdentifiers.add(appIdentifier) + } + + fun addWebDomain(webDomain: String) { + this.webDomains.add(webDomain) + } + + fun removeAppElement(appIdentifier: AppIdentifier) { + appIdentifiers.remove(appIdentifier) + } + + fun removeWebDomain(webDomain: String) { + this.webDomains.remove(webDomain) + } + + fun clear() { + appIdentifiers.clear() + webDomains.clear() + } + + fun isEmpty(): Boolean { + return appIdentifiers.isEmpty() && webDomains.isEmpty() + } +} + +@Parcelize +data class AppIdentifier( + val id: String, + val signature: String? = null, ) : Parcelable \ No newline at end of file diff --git a/database/src/main/java/com/kunzisoft/keepass/model/OriginAppEntryField.kt b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt similarity index 64% rename from database/src/main/java/com/kunzisoft/keepass/model/OriginAppEntryField.kt rename to database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt index 47ded46e0..313bb43ac 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/OriginAppEntryField.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt @@ -21,25 +21,51 @@ package com.kunzisoft.keepass.model import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.security.ProtectedString +import com.kunzisoft.keepass.model.EntryInfo.Companion.suffixFieldNamePosition -object OriginAppEntryField { +object AppOriginEntryField { const val WEB_DOMAIN_FIELD_NAME = "URL" const val APPLICATION_ID_FIELD_NAME = "AndroidApp" const val APPLICATION_SIGNATURE_FIELD_NAME = "AndroidApp Signature" /** - * Parse fields of an entry to retrieve a an OriginApp + * Parse fields of an entry to retrieve a an AppOrigin */ - fun parseFields(getField: (id: String) -> String?): OriginApp { - val appIdField = getField(APPLICATION_ID_FIELD_NAME) - val appSignatureField = getField(APPLICATION_SIGNATURE_FIELD_NAME) - val webDomainField = getField(WEB_DOMAIN_FIELD_NAME) - return OriginApp( - appId = appIdField, - appSignature = appSignatureField, - webDomain = webDomainField - ) + fun parseFields(getField: (id: String) -> String?): AppOrigin { + val appOrigin = AppOrigin() + // Get Application identifiers + generateSequence(0) { it + 1 } + .map { position -> + val appId = getField(APPLICATION_ID_FIELD_NAME + suffixFieldNamePosition(position)) + val appSignature = getField(APPLICATION_SIGNATURE_FIELD_NAME + suffixFieldNamePosition(position)) + // Pair them up, if appId is null, we stop + if (appId != null) { + appId to (appSignature ?: "") + } else { + // Stop + null + } + }.takeWhile { it != null } + .forEach { pair -> + appOrigin.addIdentifier( + AppIdentifier(pair!!.first, pair.second) + ) + } + // Get Domains + var domainFieldPosition = 0 + while (true) { + val domainKey = WEB_DOMAIN_FIELD_NAME + suffixFieldNamePosition(domainFieldPosition) + val domainValue = getField(domainKey) + if (domainValue != null) { + appOrigin.addWebDomain(domainValue) + domainFieldPosition++ + } else { + break // No more domain found + } + } + + return appOrigin } /** @@ -102,10 +128,16 @@ object OriginAppEntryField { } } - fun EntryInfo.setOriginApp(originApp: OriginApp?, customFieldsAllowed: Boolean) { - if (originApp != null) { - setApplicationId(originApp.appId, originApp.appSignature) - setWebDomain(originApp.webDomain, null, customFieldsAllowed) + /** + * Assign an AppOrigin to an EntryInfo, + * Only if [customFieldsAllowed] is true + */ + fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) { + appOrigin?.appIdentifiers?.forEach { appIdentifier -> + setApplicationId(appIdentifier.id, appIdentifier.signature) + } + appOrigin?.webDomains?.forEach { webDomain -> + setWebDomain(webDomain, null, customFieldsAllowed) } } } \ No newline at end of file diff --git a/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 52cb75021..850313cbd 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -27,10 +27,10 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Tags import com.kunzisoft.keepass.database.element.entry.AutoType +import com.kunzisoft.keepass.model.AppOriginEntryField.setAppOrigin +import com.kunzisoft.keepass.model.AppOriginEntryField.setApplicationId +import com.kunzisoft.keepass.model.AppOriginEntryField.setWebDomain import com.kunzisoft.keepass.model.CreditCardEntryFields.setCreditCard -import com.kunzisoft.keepass.model.OriginAppEntryField.setApplicationId -import com.kunzisoft.keepass.model.OriginAppEntryField.setOriginApp -import com.kunzisoft.keepass.model.OriginAppEntryField.setWebDomain import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion import com.kunzisoft.keepass.model.PasskeyEntryFields.setPasskey import com.kunzisoft.keepass.otp.OtpElement @@ -59,7 +59,7 @@ class EntryInfo : NodeInfo { var autoType: AutoType = AutoType() var otpModel: OtpModel? = null var passkey: Passkey? = null - var originApp: OriginApp? = null + var appOrigin: AppOrigin? = null var isTemplate: Boolean = false constructor() : super() @@ -80,7 +80,7 @@ class EntryInfo : NodeInfo { autoType = parcel.readParcelableCompat() ?: autoType otpModel = parcel.readParcelableCompat() ?: otpModel passkey = parcel.readParcelableCompat() ?: passkey - originApp = parcel.readParcelableCompat() ?: originApp + appOrigin = parcel.readParcelableCompat() ?: appOrigin isTemplate = parcel.readBooleanCompat() } @@ -103,7 +103,7 @@ class EntryInfo : NodeInfo { parcel.writeParcelable(autoType, flags) parcel.writeParcelable(otpModel, flags) parcel.writeParcelable(passkey, flags) - parcel.writeParcelable(originApp, flags) + parcel.writeParcelable(appOrigin, flags) parcel.writeBooleanCompat(isTemplate) } @@ -138,13 +138,6 @@ class EntryInfo : NodeInfo { } ?: customFields.add(field) } - /** - * Create a field name suffix depending on the field position - */ - private fun suffixFieldNamePosition(position: Int): String { - return if (position > 0) "_$position" else "" - } - /** * Add a field to the custom fields list with a suffix position, * replace if name already exists @@ -218,8 +211,8 @@ class EntryInfo : NodeInfo { registerInfo.password?.let { password = it } setCreditCard(registerInfo.creditCard) setPasskey(registerInfo.passkey) - setOriginApp( - registerInfo.originApp, + setAppOrigin( + registerInfo.appOrigin, database?.allowEntryCustomFields() == true ) } @@ -250,7 +243,7 @@ class EntryInfo : NodeInfo { if (autoType != other.autoType) return false if (otpModel != other.otpModel) return false if (passkey != other.passkey) return false - if (originApp != other.originApp) return false + if (appOrigin != other.appOrigin) return false if (isTemplate != other.isTemplate) return false return true @@ -271,7 +264,7 @@ class EntryInfo : NodeInfo { result = 31 * result + autoType.hashCode() result = 31 * result + (otpModel?.hashCode() ?: 0) result = 31 * result + (passkey?.hashCode() ?: 0) - result = 31 * result + (originApp?.hashCode() ?: 0) + result = 31 * result + (appOrigin?.hashCode() ?: 0) result = 31 * result + isTemplate.hashCode() return result } @@ -279,6 +272,13 @@ class EntryInfo : NodeInfo { companion object { + /** + * Create a field name suffix depending on the field position + */ + fun suffixFieldNamePosition(position: Int): String { + return if (position > 0) "_$position" else "" + } + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): EntryInfo { diff --git a/database/src/main/java/com/kunzisoft/keepass/model/RegisterInfo.kt b/database/src/main/java/com/kunzisoft/keepass/model/RegisterInfo.kt index 3691fe83e..a475b6955 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/RegisterInfo.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/RegisterInfo.kt @@ -10,7 +10,7 @@ data class RegisterInfo( val password: String? = null, val creditCard: CreditCard? = null, val passkey: Passkey? = null, - val originApp: OriginApp? = null + val appOrigin: AppOrigin? = null ): Parcelable { constructor(parcel: Parcel) : this( @@ -19,7 +19,7 @@ data class RegisterInfo( password = parcel.readString() ?: "", creditCard = parcel.readParcelableCompat(), passkey = parcel.readParcelableCompat(), - originApp = parcel.readParcelableCompat() + appOrigin = parcel.readParcelableCompat() ) override fun writeToParcel(parcel: Parcel, flags: Int) { @@ -28,7 +28,7 @@ data class RegisterInfo( parcel.writeString(password) parcel.writeParcelable(creditCard, flags) parcel.writeParcelable(passkey, flags) - parcel.writeParcelable(originApp, flags) + parcel.writeParcelable(appOrigin, flags) } override fun describeContents(): Int {