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") Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters( retrievePasskeyUsageRequestParameters(
intent = intent, intent = intent,
assetManager = assets, assetManager = assets
appOrigin = appOrigin,
) { usageParameters -> ) { usageParameters ->
// Save the requested parameters // Save the requested parameters
mUsageParameters = usageParameters mUsageParameters = usageParameters

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.data
import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException
import com.kunzisoft.asymmetric.Signature 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 import org.json.JSONObject
class AuthenticatorAssertionResponse( class AuthenticatorAssertionResponse(

View File

@@ -19,7 +19,7 @@
*/ */
package com.kunzisoft.keepass.credentialprovider.passkey.data 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 com.kunzisoft.keepass.utils.UUIDUtils.asBytes
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.credentialprovider.passkey.data package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.HashManager 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 import org.json.JSONObject
open class ClientDataBuildResponse( open class ClientDataBuildResponse(

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,23 @@
package com.kunzisoft.encrypt
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

@@ -118,7 +118,7 @@ object HashManager {
return StreamCipher(cipher) return StreamCipher(cipher)
} }
private const val SIGNATURE_DELIMITER = "##SIG##" const val SIGNATURE_DELIMITER = "##SIG##"
/** /**
* Converts a Signature object into its SHA-256 fingerprint string. * Converts a Signature object into its SHA-256 fingerprint string.
@@ -149,7 +149,7 @@ object HashManager {
* @param signingInfo The SigningInfo object to retrieve the strings signatures * @param signingInfo The SigningInfo object to retrieve the strings signatures
* @return A List of SHA-256 fingerprint strings, or null if an error occurs or no signatures are found. * @return A List of SHA-256 fingerprint strings, or null if an error occurs or no signatures are found.
*/ */
fun getAllSignatures(signingInfo: SigningInfo?): List<String>? { fun getAllFingerprints(signingInfo: SigningInfo?): List<String>? {
try { try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
throw AndroidException("API level ${Build.VERSION.SDK_INT} not supported") throw AndroidException("API level ${Build.VERSION.SDK_INT} not supported")
@@ -181,13 +181,51 @@ object HashManager {
/** /**
* Combines a list of signature into a single string for database storage. * Combines a list of signature into a single string for database storage.
* *
* @return A single string with fingerprints joined by a delimiter, or null if the input list is null or empty. * @return A single string with fingerprints joined by a ##SIG## delimiter,
* or null if the input list is null or empty.
*/ */
fun SigningInfo.getApplicationSignatures(): String? { fun SigningInfo.getApplicationFingerprints(): String? {
val fingerprints = getAllSignatures(this) val fingerprints = getAllFingerprints(this)
if (fingerprints.isNullOrEmpty()) { if (fingerprints.isNullOrEmpty()) {
return null return null
} }
return fingerprints.joinToString(SIGNATURE_DELIMITER) return fingerprints.joinToString(SIGNATURE_DELIMITER)
} }
/**
* Transforms a colon-separated hex fingerprint string into a URL-safe,
* padding-removed Base64 string, mimicking the Python behavior:
* base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', '')
*
* Only check the first footprint if there are several delimited by ##SIG##.
*
* @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:...").
* @return The Android App Origin string.
* @throws IllegalArgumentException if the hex string (after removing colons) has an odd length
* or contains non-hex characters.
*/
fun fingerprintToUrlSafeBase64(fingerprint: String): String {
val firstFingerprint = fingerprint.split(SIGNATURE_DELIMITER).firstOrNull()?.trim()
if (firstFingerprint.isNullOrEmpty()) {
throw IllegalArgumentException("Invalid fingerprint $fingerprint")
}
val hexStringNoColons = fingerprint.replace(":", "")
if (hexStringNoColons.length % 2 != 0) {
throw IllegalArgumentException("Hex string must have an even number of characters: $hexStringNoColons")
}
if (hexStringNoColons.length != 64) {
throw IllegalArgumentException("Expected a 64-character hex string for a SHA-256 hash, but got ${hexStringNoColons.length} characters.")
}
val hashBytes = ByteArray(hexStringNoColons.length / 2)
for (i in hashBytes.indices) {
try {
val index = i * 2
val byteValue = hexStringNoColons.substring(index, index + 2).toInt(16)
hashBytes[i] = byteValue.toByte()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("Invalid hex character in fingerprint: $hexStringNoColons", e)
}
}
return Base64Helper.b64Encode(hashBytes)
}
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.model
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -36,44 +37,22 @@ data class AppOrigin(
this.webOrigins.add(webOrigin) this.webOrigins.add(webOrigin)
} }
fun containsVerifiedAndroidOrigin(androidOrigin: AndroidOrigin): Boolean { val verified: Boolean
return androidOrigins.any { get() = androidOrigins.any { it.verified }
it.packageName == androidOrigin.packageName
&& it.signature == androidOrigin.signature
&& it.verification.verified
}
}
fun getFirstAndroidOrigin(): AndroidOrigin? { /**
return androidOrigins.firstOrNull() * 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 containsVerifiedWebOrigin(webOrigin: WebOrigin): Boolean { fun checkAppOrigin(appToCheck: AppOrigin): String? {
return this.webOrigins.any { return androidOrigins.firstOrNull { androidOrigin ->
it.origin == webOrigin.origin appToCheck.androidOrigins.any {
&& it.verification.verified it.packageName == androidOrigin.packageName
} && it.fingerprint == androidOrigin.fingerprint
} }
}?.let {
fun containsUnverifiedWebOrigin(): Boolean { AndroidOrigin(it.packageName, it.fingerprint, true)
return this.webOrigins.any { .toAndroidOrigin()
it.verification.verified.not()
}
}
fun firstVerifiedWebOrigin(): WebOrigin? {
return webOrigins.first {
it.verification.verified
}
}
fun getFirstWebOrigin(): WebOrigin? {
return webOrigins.firstOrNull()
}
fun firstUnverifiedOrigin(): WebOrigin? {
return webOrigins.first {
it.verification.verified.not()
} }
} }
@@ -86,6 +65,11 @@ data class AppOrigin(
return androidOrigins.isEmpty() && webOrigins.isEmpty() return androidOrigins.isEmpty() && webOrigins.isEmpty()
} }
fun toAppOrigin(): String {
return androidOrigins.firstOrNull()?.toAndroidOrigin()
?: throw SecurityException("No app origin found")
}
fun toName(): String? { fun toName(): String? {
return if (androidOrigins.isNotEmpty()) { return if (androidOrigins.isNotEmpty()) {
androidOrigins.first().packageName androidOrigins.first().packageName
@@ -95,29 +79,36 @@ data class AppOrigin(
} }
} }
enum class Verification {
MANUALLY_VERIFIED, AUTOMATICALLY_VERIFIED, NOT_VERIFIED;
val verified: Boolean
get() = this == MANUALLY_VERIFIED || this == AUTOMATICALLY_VERIFIED
}
@Parcelize @Parcelize
data class AndroidOrigin( data class AndroidOrigin(
val packageName: String, val packageName: String,
val signature: String? = null, val fingerprint: String?,
val verification: Verification = Verification.AUTOMATICALLY_VERIFIED, val verified: Boolean = true
) : Parcelable { ) : Parcelable {
/**
* Creates an Android App Origin string of the form "android:apk-key-hash:<base64_urlsafe_hash>"
* from a colon-separated hex fingerprint string.
*
* The input fingerprint is assumed to be the SHA-256 hash of the app's signing certificate.
*
* @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:...").
* @return The Android App Origin string.
* @throws IllegalArgumentException if the hex string (after removing colons) has an odd length
* or contains non-hex characters.
*/
fun toAndroidOrigin(): String { fun toAndroidOrigin(): String {
return "android:apk-key-hash:${packageName}" if (fingerprint == null) {
throw IllegalArgumentException("Fingerprint $fingerprint cannot be null")
}
return "android:apk-key-hash:${fingerprintToUrlSafeBase64(fingerprint)}"
} }
} }
@Parcelize @Parcelize
data class WebOrigin( data class WebOrigin(
val origin: String, val origin: String,
val verification: Verification = Verification.AUTOMATICALLY_VERIFIED, val verified: Boolean = true,
) : Parcelable { ) : Parcelable {
fun toWebOrigin(): String { fun toWebOrigin(): String {
@@ -130,9 +121,9 @@ data class WebOrigin(
companion object { companion object {
const val RELYING_PARTY_DEFAULT_PROTOCOL = "https" const val RELYING_PARTY_DEFAULT_PROTOCOL = "https"
fun fromRelyingParty(relyingParty: String, verification: Verification): WebOrigin = WebOrigin( fun fromRelyingParty(relyingParty: String, verified: Boolean): WebOrigin = WebOrigin(
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty", origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty",
verification = verification verified = verified
) )
} }
} }

View File

@@ -138,10 +138,10 @@ object AppOriginEntryField {
*/ */
fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) { fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) {
appOrigin?.androidOrigins?.forEach { appIdentifier -> appOrigin?.androidOrigins?.forEach { appIdentifier ->
setApplicationId(appIdentifier.packageName, appIdentifier.signature) setApplicationId(appIdentifier.packageName, appIdentifier.fingerprint)
} }
appOrigin?.webOrigins?.forEach { webOrigin -> appOrigin?.webOrigins?.forEach { webOrigin ->
if (webOrigin.verification.verified) if (webOrigin.verified)
setWebDomain(webOrigin.origin, null, customFieldsAllowed) setWebDomain(webOrigin.origin, null, customFieldsAllowed)
} }
} }