fix: Change Android origin

This commit is contained in:
J-Jamet
2025-09-01 14:48:36 +02:00
parent 0d133ffdb0
commit 200881278c
14 changed files with 158 additions and 172 deletions

View File

@@ -234,8 +234,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(
intent = intent,
assetManager = assets,
appOrigin = appOrigin,
assetManager = assets
) { usageParameters ->
// Save the requested parameters
mUsageParameters = usageParameters

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.data
import androidx.credentials.exceptions.GetCredentialUnknownException
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import org.json.JSONObject
class AuthenticatorAssertionResponse(

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
import org.json.JSONArray
import org.json.JSONObject

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import org.json.JSONObject
open class ClientDataBuildResponse(

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.credentialprovider.passkey.data
import android.util.Log
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper
import com.kunzisoft.encrypt.Base64Helper
import org.json.JSONObject
class PublicKeyCredentialCreationOptions(

View File

@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper
import com.kunzisoft.encrypt.Base64Helper
import org.json.JSONObject
class PublicKeyCredentialRequestOptions(requestJson: String) {

View File

@@ -19,12 +19,10 @@
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.WebOrigin
import com.kunzisoft.keepass.model.AppOrigin
data class PublicKeyCredentialUsageParameters(
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
val clientDataResponse: ClientDataResponse,
val androidOrigin: AndroidOrigin,
val webOrigin: WebOrigin,
val appOrigin: AppOrigin
)

View File

@@ -1,42 +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.util.Base64
class Base64Helper {
companion object {
fun b64Decode(encodedString: String): ByteArray {
return Base64.decode(
encodedString,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
)
}
fun b64Encode(data: ByteArray): String {
return Base64.encodeToString(
data,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
)
}
}
}

View File

@@ -24,10 +24,9 @@ import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
import com.kunzisoft.encrypt.HashManager.getApplicationSignatures
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.Verification
import com.kunzisoft.keepass.model.WebOrigin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -81,27 +80,26 @@ class OriginManager(
* calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
*/
suspend fun getOriginAtUsage(
appOrigin: AppOrigin,
onOriginRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin) -> Unit
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (appOrigin: AppOrigin) -> Unit
) {
getOrigin(
onOriginRetrieved = { androidOrigin, webOrigin, origin, clientDataHash ->
onOriginRetrieved(androidOrigin, webOrigin, clientDataHash)
onOriginRetrieved(
AppOrigin().apply {
addAndroidOrigin(androidOrigin)
addWebOrigin(webOrigin)
},
clientDataHash
)
},
onOriginNotRetrieved = { appIdentifierToCheck, webOrigin ->
onOriginNotRetrieved = { androidOrigin, webOrigin ->
// Check the app signature in the appOrigin, webOrigin cannot be checked now
onOriginCreated(
AndroidOrigin(
packageName = appIdentifierToCheck.packageName,
signature = appIdentifierToCheck.signature,
verification =
if (appOrigin.containsVerifiedAndroidOrigin(appIdentifierToCheck))
Verification.MANUALLY_VERIFIED
else
Verification.NOT_VERIFIED
),
webOrigin
AppOrigin().apply {
addAndroidOrigin(androidOrigin)
addWebOrigin(webOrigin)
}
)
}
)
@@ -129,23 +127,27 @@ class OriginManager(
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
val androidOrigin = AndroidOrigin(
packageName = callingAppInfo.packageName,
signature = callingAppInfo.signingInfo
.getApplicationSignatures(),
verification = Verification.NOT_VERIFIED
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,
signature = androidOrigin.signature,
verification = Verification.AUTOMATICALLY_VERIFIED
fingerprint = androidOrigin.fingerprint,
verified = true
),
WebOrigin.fromRelyingParty(
relyingParty = relyingParty,
verification = Verification.AUTOMATICALLY_VERIFIED
WebOrigin(
origin = webOrigin.origin,
verified = true
),
callOrigin,
providedClientDataHash
@@ -153,10 +155,7 @@ class OriginManager(
} else {
onOriginNotRetrieved(
androidOrigin,
WebOrigin.fromRelyingParty(
relyingParty = relyingParty,
verification = Verification.NOT_VERIFIED
)
webOrigin
)
}
}

View File

@@ -39,6 +39,7 @@ 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.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -50,12 +51,10 @@ 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.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
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.Verification
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.random.KeePassDXRandom
@@ -436,13 +435,11 @@ object PasskeyHelper {
* Utility method to use a passkey and create the associated usage request parameters
* [intent] allows to retrieve the request
* [assetManager] has been transferred to the origin manager to manage package verification files
* [appOrigin] retrieves the origin params stored in an entry, which may be null if not found or if the database is closed.
* [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
*/
suspend fun retrievePasskeyUsageRequestParameters(
intent: Intent,
assetManager: AssetManager,
appOrigin: AppOrigin,
result: (PublicKeyCredentialUsageParameters) -> Unit
) {
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
@@ -460,18 +457,16 @@ object PasskeyHelper {
assets = assetManager,
relyingParty = requestOptions.rpId
).getOriginAtUsage(
appOrigin = appOrigin,
onOriginRetrieved = { androidOrigin, webOrigin, clientDataHash ->
onOriginRetrieved = { appOrigin, clientDataHash ->
result.invoke(
PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
androidOrigin = androidOrigin,
webOrigin = webOrigin
appOrigin = appOrigin
)
)
},
onOriginCreated = { androidOrigin, webOrigin ->
onOriginCreated = { appOrigin ->
// By default we crate an usage parameter with Android origin
result.invoke(
PublicKeyCredentialUsageParameters(
@@ -479,10 +474,9 @@ object PasskeyHelper {
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = requestOptions.challenge,
origin = androidOrigin.toAndroidOrigin()
origin = appOrigin.toAppOrigin()
),
androidOrigin = androidOrigin,
webOrigin = webOrigin
appOrigin = appOrigin
)
)
}
@@ -519,37 +513,23 @@ object PasskeyHelper {
/**
* Verify that the application signature is contained in the [appOrigin]
* or that the webDomain contains the origin
*/
fun getVerifiedGETClientDataResponse(
usageParameters: PublicKeyCredentialUsageParameters,
appOrigin: AppOrigin
): ClientDataResponse {
val appToCheck = usageParameters.androidOrigin
val webToCheck = usageParameters.webOrigin
if (appToCheck.verification == Verification.AUTOMATICALLY_VERIFIED) {
return usageParameters.clientDataResponse
val appToCheck = usageParameters.appOrigin
return if (appToCheck.verified) {
usageParameters.clientDataResponse
} else {
if (appOrigin.containsVerifiedAndroidOrigin(appToCheck)) {
if (webToCheck.verification.verified
|| appOrigin.containsVerifiedWebOrigin(webToCheck)) {
// Origin checked by URL
return ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
origin = webToCheck.toWebOrigin()
)
}
appOrigin.checkAppOrigin(appToCheck)?.let { origin ->
// Origin checked by Android app signature
return ClientDataBuildResponse(
ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
origin = appOrigin.firstVerifiedWebOrigin()?.toWebOrigin()
?: appToCheck.toAndroidOrigin()
origin = origin
)
} else {
throw SecurityException("Wrong signature for ${appToCheck.packageName}")
}
} ?: throw SecurityException("Wrong signature for $appToCheck")
}
}
}