fix: Allow to check multiple app signatures #1421

This commit is contained in:
J-Jamet
2025-08-27 23:09:51 +02:00
parent fcf723849b
commit 5f27f161a5
13 changed files with 311 additions and 164 deletions

View File

@@ -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<Intent>? =
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)
},

View File

@@ -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(

View File

@@ -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
)

View File

@@ -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
}
}
}

View File

@@ -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
)
)
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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

View File

@@ -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<Group> {
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<Group> {
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)

View File

@@ -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<AppIdentifier> = mutableListOf(),
val webDomains: MutableList<String> = 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

View File

@@ -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)
}
}
}

View File

@@ -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<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
override fun createFromParcel(parcel: Parcel): EntryInfo {

View File

@@ -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 {