mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Allow to check multiple app signatures #1421
This commit is contained in:
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user