mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Refactoring verification
This commit is contained in:
@@ -170,7 +170,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
||||
finish()
|
||||
}) {
|
||||
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin()
|
||||
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
||||
val nodeId = intent.retrieveNodeId()
|
||||
checkSecurity(intent, nodeId)
|
||||
when (mSpecialMode) {
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
|
||||
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||
import com.kunzisoft.keepass.model.AppOrigin
|
||||
import com.kunzisoft.keepass.model.WebOrigin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Utility class to manage the origin of credential provider applications
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
class OriginManager(
|
||||
private val providedClientDataHash: ByteArray?,
|
||||
private val callingAppInfo: CallingAppInfo?,
|
||||
private val assets: AssetManager,
|
||||
private val relyingParty: String,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Retrieves the Android origin to be stored in the database,
|
||||
* call [onOriginRetrieved] if the origin is already calculated by the system
|
||||
* call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
|
||||
*/
|
||||
suspend fun getOriginAtCreation(
|
||||
onOriginRetrieved: (appInfoToStore: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||
onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit
|
||||
) {
|
||||
getOrigin(
|
||||
onOriginRetrieved = { androidOrigin, webOrigin, callOrigin, clientDataHash ->
|
||||
onOriginRetrieved(
|
||||
AppOrigin().apply {
|
||||
addAndroidOrigin(androidOrigin)
|
||||
addWebOrigin(webOrigin)
|
||||
},
|
||||
clientDataHash
|
||||
)
|
||||
},
|
||||
onOriginNotRetrieved = { appIdentifier, webOrigin ->
|
||||
// Create a new Android Origin and prepare the signature app storage
|
||||
onOriginCreated(
|
||||
AppOrigin().apply {
|
||||
addAndroidOrigin(appIdentifier)
|
||||
addWebOrigin(webOrigin)
|
||||
},
|
||||
appIdentifier.toAndroidOrigin()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the origin to verify usage,
|
||||
* calls [onOriginRetrieved] if the origin is already calculated by the system
|
||||
* calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
|
||||
*/
|
||||
suspend fun getOriginAtUsage(
|
||||
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||
onOriginCreated: (appOrigin: AppOrigin) -> Unit
|
||||
) {
|
||||
getOrigin(
|
||||
onOriginRetrieved = { androidOrigin, webOrigin, origin, clientDataHash ->
|
||||
onOriginRetrieved(
|
||||
AppOrigin().apply {
|
||||
addAndroidOrigin(androidOrigin)
|
||||
addWebOrigin(webOrigin)
|
||||
},
|
||||
clientDataHash
|
||||
)
|
||||
},
|
||||
onOriginNotRetrieved = { androidOrigin, webOrigin ->
|
||||
// Check the app signature in the appOrigin, webOrigin cannot be checked now
|
||||
onOriginCreated(
|
||||
AppOrigin().apply {
|
||||
addAndroidOrigin(androidOrigin)
|
||||
addWebOrigin(webOrigin)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to retrieve the origin asynchronously,
|
||||
* checks for the presence of the application in the privilege list of the trustedPackages.json file,
|
||||
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list
|
||||
* call [onOriginNotRetrieved] if the origin is not retrieved from the system
|
||||
*/
|
||||
private suspend fun getOrigin(
|
||||
onOriginRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin, origin: String, clientDataHash: ByteArray) -> Unit,
|
||||
onOriginNotRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin) -> Unit
|
||||
) {
|
||||
if (callingAppInfo == null) {
|
||||
throw SecurityException("Calling app info cannot be retrieved")
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
var callOrigin: String?
|
||||
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
// for trusted browsers like Chrome and Firefox
|
||||
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
||||
val androidOrigin = AndroidOrigin(
|
||||
packageName = callingAppInfo.packageName,
|
||||
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints(),
|
||||
verified = false
|
||||
)
|
||||
val webOrigin = WebOrigin.fromRelyingParty(
|
||||
relyingParty = relyingParty,
|
||||
verified = false
|
||||
)
|
||||
// Check if the webDomain is validated for the
|
||||
withContext(Dispatchers.Main) {
|
||||
if (callOrigin != null && providedClientDataHash != null) {
|
||||
Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo")
|
||||
// TODO verified callOrigin
|
||||
onOriginRetrieved(
|
||||
AndroidOrigin(
|
||||
packageName = androidOrigin.packageName,
|
||||
fingerprint = androidOrigin.fingerprint,
|
||||
verified = true
|
||||
),
|
||||
WebOrigin(
|
||||
origin = webOrigin.origin,
|
||||
verified = true
|
||||
),
|
||||
callOrigin,
|
||||
providedClientDataHash
|
||||
)
|
||||
} else {
|
||||
onOriginNotRetrieved(
|
||||
androidOrigin,
|
||||
webOrigin
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = OriginManager::class.simpleName
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,13 @@ import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.PublicKeyCredential
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.kunzisoft.asymmetric.Signature
|
||||
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||
@@ -51,13 +53,17 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||
import com.kunzisoft.keepass.model.AppOrigin
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Passkey
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.model.WebOrigin
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.random.KeePassDXRandom
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.time.Instant
|
||||
@@ -320,6 +326,59 @@ object PasskeyHelper {
|
||||
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to retrieve the origin asynchronously,
|
||||
* checks for the presence of the application in the privilege list of the trustedPackages.json file,
|
||||
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
|
||||
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
|
||||
*/
|
||||
suspend fun getOrigin(
|
||||
providedClientDataHash: ByteArray?,
|
||||
callingAppInfo: CallingAppInfo?,
|
||||
assets: AssetManager,
|
||||
relyingParty: String,
|
||||
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
|
||||
) {
|
||||
if (callingAppInfo == null) {
|
||||
throw SecurityException("Calling app info cannot be retrieved")
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
var callOrigin: String?
|
||||
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
// for trusted browsers like Chrome and Firefox
|
||||
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
||||
val androidOrigin = AndroidOrigin(
|
||||
packageName = callingAppInfo.packageName,
|
||||
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
|
||||
)
|
||||
val webOrigin = WebOrigin.fromRelyingParty(
|
||||
relyingParty = relyingParty
|
||||
)
|
||||
// Check if the webDomain is validated for the
|
||||
withContext(Dispatchers.Main) {
|
||||
if (callOrigin != null && providedClientDataHash != null) {
|
||||
// Origin already defined by the system
|
||||
Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
|
||||
onOriginRetrieved(
|
||||
AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
|
||||
providedClientDataHash
|
||||
)
|
||||
} else {
|
||||
// Add Android origin by default
|
||||
onOriginNotRetrieved(
|
||||
AppOrigin(verified = false).apply {
|
||||
addAndroidOrigin(androidOrigin)
|
||||
},
|
||||
androidOrigin.toAndroidOrigin()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create a passkey and the associated creation request parameters
|
||||
* [intent] allows to retrieve the request
|
||||
@@ -360,12 +419,11 @@ object PasskeyHelper {
|
||||
)
|
||||
|
||||
// create new entry in database
|
||||
OriginManager(
|
||||
getOrigin(
|
||||
providedClientDataHash = clientDataHash,
|
||||
callingAppInfo = callingAppInfo,
|
||||
assets = assetManager,
|
||||
relyingParty = relyingParty
|
||||
).getOriginAtCreation(
|
||||
relyingParty = relyingParty,
|
||||
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
||||
passkeyCreated.invoke(
|
||||
passkey,
|
||||
@@ -378,7 +436,7 @@ object PasskeyHelper {
|
||||
)
|
||||
)
|
||||
},
|
||||
onOriginCreated = { appInfoToStore, origin ->
|
||||
onOriginNotRetrieved = { appInfoToStore, origin ->
|
||||
passkeyCreated.invoke(
|
||||
passkey,
|
||||
appInfoToStore,
|
||||
@@ -451,12 +509,11 @@ object PasskeyHelper {
|
||||
|
||||
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
|
||||
|
||||
OriginManager(
|
||||
getOrigin(
|
||||
providedClientDataHash = clientDataHash,
|
||||
callingAppInfo = callingAppInfo,
|
||||
assets = assetManager,
|
||||
relyingParty = requestOptions.rpId
|
||||
).getOriginAtUsage(
|
||||
relyingParty = requestOptions.rpId,
|
||||
onOriginRetrieved = { appOrigin, clientDataHash ->
|
||||
result.invoke(
|
||||
PublicKeyCredentialUsageParameters(
|
||||
@@ -466,7 +523,7 @@ object PasskeyHelper {
|
||||
)
|
||||
)
|
||||
},
|
||||
onOriginCreated = { appOrigin ->
|
||||
onOriginNotRetrieved = { appOrigin, androidOriginString ->
|
||||
// By default we crate an usage parameter with Android origin
|
||||
result.invoke(
|
||||
PublicKeyCredentialUsageParameters(
|
||||
@@ -474,7 +531,7 @@ object PasskeyHelper {
|
||||
clientDataResponse = ClientDataBuildResponse(
|
||||
type = ClientDataBuildResponse.Type.GET,
|
||||
challenge = requestOptions.challenge,
|
||||
origin = appOrigin.toAppOrigin()
|
||||
origin = androidOriginString
|
||||
),
|
||||
appOrigin = appOrigin
|
||||
)
|
||||
@@ -522,7 +579,7 @@ object PasskeyHelper {
|
||||
return if (appToCheck.verified) {
|
||||
usageParameters.clientDataResponse
|
||||
} else {
|
||||
appOrigin.checkAppOrigin(appToCheck)?.let { origin ->
|
||||
appToCheck.checkAppOrigin(appOrigin)?.let { origin ->
|
||||
// Origin checked by Android app signature
|
||||
ClientDataBuildResponse(
|
||||
type = ClientDataBuildResponse.Type.GET,
|
||||
|
||||
@@ -20,13 +20,16 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
|
||||
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AppOrigin(
|
||||
val verified: Boolean,
|
||||
val androidOrigins: MutableList<AndroidOrigin> = mutableListOf(),
|
||||
val webOrigins: MutableList<WebOrigin> = mutableListOf()
|
||||
val webOrigins: MutableList<WebOrigin> = mutableListOf(),
|
||||
) : Parcelable {
|
||||
|
||||
fun addAndroidOrigin(androidOrigin: AndroidOrigin) {
|
||||
@@ -37,22 +40,21 @@ data class AppOrigin(
|
||||
this.webOrigins.add(webOrigin)
|
||||
}
|
||||
|
||||
val verified: Boolean
|
||||
get() = androidOrigins.any { it.verified }
|
||||
|
||||
/**
|
||||
* Verify the app origin by comparing it to the list of android origins,
|
||||
* return the first verified origin or null if none is found
|
||||
*/
|
||||
fun checkAppOrigin(appToCheck: AppOrigin): String? {
|
||||
fun checkAppOrigin(compare: AppOrigin): String? {
|
||||
return androidOrigins.firstOrNull { androidOrigin ->
|
||||
appToCheck.androidOrigins.any {
|
||||
compare.androidOrigins.any {
|
||||
it.packageName == androidOrigin.packageName
|
||||
&& it.fingerprint == androidOrigin.fingerprint
|
||||
}
|
||||
}?.let {
|
||||
AndroidOrigin(it.packageName, it.fingerprint, true)
|
||||
.toAndroidOrigin()
|
||||
AndroidOrigin(
|
||||
packageName = it.packageName,
|
||||
fingerprint = it.fingerprint
|
||||
).toAndroidOrigin()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +67,6 @@ data class AppOrigin(
|
||||
return androidOrigins.isEmpty() && webOrigins.isEmpty()
|
||||
}
|
||||
|
||||
fun toAppOrigin(): String {
|
||||
return androidOrigins.firstOrNull()?.toAndroidOrigin()
|
||||
?: throw SecurityException("No app origin found")
|
||||
}
|
||||
|
||||
fun toName(): String? {
|
||||
return if (androidOrigins.isNotEmpty()) {
|
||||
androidOrigins.first().packageName
|
||||
@@ -77,13 +74,32 @@ data class AppOrigin(
|
||||
webOrigins.first().origin
|
||||
} else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = AppOrigin::class.java.simpleName
|
||||
|
||||
fun fromOrigin(origin: String, androidOrigin: AndroidOrigin, verified: Boolean): AppOrigin {
|
||||
val appOrigin = AppOrigin(verified)
|
||||
if (origin.startsWith(RELYING_PARTY_DEFAULT_PROTOCOL)) {
|
||||
appOrigin.apply {
|
||||
addWebOrigin(WebOrigin(origin))
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Unknown verified origin $origin")
|
||||
appOrigin.apply {
|
||||
addAndroidOrigin(androidOrigin)
|
||||
}
|
||||
}
|
||||
return appOrigin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AndroidOrigin(
|
||||
val packageName: String,
|
||||
val fingerprint: String?,
|
||||
val verified: Boolean = true
|
||||
val fingerprint: String?
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
@@ -107,8 +123,7 @@ data class AndroidOrigin(
|
||||
|
||||
@Parcelize
|
||||
data class WebOrigin(
|
||||
val origin: String,
|
||||
val verified: Boolean = true,
|
||||
val origin: String
|
||||
) : Parcelable {
|
||||
|
||||
fun toWebOrigin(): String {
|
||||
@@ -121,9 +136,8 @@ data class WebOrigin(
|
||||
|
||||
companion object {
|
||||
const val RELYING_PARTY_DEFAULT_PROTOCOL = "https"
|
||||
fun fromRelyingParty(relyingParty: String, verified: Boolean): WebOrigin = WebOrigin(
|
||||
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty",
|
||||
verified = verified
|
||||
fun fromRelyingParty(relyingParty: String): WebOrigin = WebOrigin(
|
||||
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ object AppOriginEntryField {
|
||||
* Parse fields of an entry to retrieve a an AppOrigin
|
||||
*/
|
||||
fun parseFields(getField: (id: String) -> String?): AppOrigin {
|
||||
val appOrigin = AppOrigin()
|
||||
val appOrigin = AppOrigin(verified = true)
|
||||
// Get Application identifiers
|
||||
generateSequence(0) { it + 1 }
|
||||
.map { position ->
|
||||
@@ -141,7 +141,6 @@ object AppOriginEntryField {
|
||||
setApplicationId(appIdentifier.packageName, appIdentifier.fingerprint)
|
||||
}
|
||||
appOrigin?.webOrigins?.forEach { webOrigin ->
|
||||
if (webOrigin.verified)
|
||||
setWebDomain(webOrigin.origin, null, customFieldsAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user