mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Change Android origin
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
23
crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt
Normal file
23
crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user