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