mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
feat: JSON parser to manage Privileged apps
This commit is contained in:
@@ -234,7 +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
|
context = applicationContext
|
||||||
) { usageParameters ->
|
) { usageParameters ->
|
||||||
// Save the requested parameters
|
// Save the requested parameters
|
||||||
mUsageParameters = usageParameters
|
mUsageParameters = usageParameters
|
||||||
@@ -304,7 +304,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
Log.d(TAG, "Launch passkey registration")
|
Log.d(TAG, "Launch passkey registration")
|
||||||
retrievePasskeyCreationRequestParameters(
|
retrievePasskeyCreationRequestParameters(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
assetManager = assets,
|
context = applicationContext,
|
||||||
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
||||||
// Save the requested parameters
|
// Save the requested parameters
|
||||||
mPasskey = passkey
|
mPasskey = passkey
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 AOSP modified by 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.data
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an Android privileged app, based on AOSP code
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class AndroidPrivilegedApp(
|
||||||
|
val packageName: String,
|
||||||
|
val fingerprints: Set<String>
|
||||||
|
): Parcelable {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$packageName ($fingerprints)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PACKAGE_NAME_KEY = "package_name"
|
||||||
|
private const val SIGNATURES_KEY = "signatures"
|
||||||
|
private const val FINGERPRINT_KEY = "cert_fingerprint_sha256"
|
||||||
|
private const val BUILD_KEY = "build"
|
||||||
|
private const val USER_DEBUG_KEY = "userdebug"
|
||||||
|
private const val TYPE_KEY = "type"
|
||||||
|
private const val APP_INFO_KEY = "info"
|
||||||
|
private const val ANDROID_TYPE_KEY = "android"
|
||||||
|
private const val USER_BUILD_TYPE = "userdebug"
|
||||||
|
private const val APPS_KEY = "apps"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun extractPrivilegedApps(jsonObject: JSONObject): List<AndroidPrivilegedApp> {
|
||||||
|
val apps = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
if (!jsonObject.has(APPS_KEY)) {
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
val appsJsonArray = jsonObject.getJSONArray(APPS_KEY)
|
||||||
|
for (i in 0 until appsJsonArray.length()) {
|
||||||
|
try {
|
||||||
|
val appJsonObject = appsJsonArray.getJSONObject(i)
|
||||||
|
if (appJsonObject.getString(TYPE_KEY) != ANDROID_TYPE_KEY) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!appJsonObject.has(APP_INFO_KEY)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apps.add(
|
||||||
|
createFromJSONObject(
|
||||||
|
appJsonObject.getJSONObject(APP_INFO_KEY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.e(AndroidPrivilegedApp::class.simpleName, "Error parsing privileged app", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AndroidPrivilegedApp object from a JSONObject.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun createFromJSONObject(
|
||||||
|
appInfoJsonObject: JSONObject,
|
||||||
|
filterUserDebug: Boolean = true
|
||||||
|
): AndroidPrivilegedApp {
|
||||||
|
val signaturesJson = appInfoJsonObject.getJSONArray(SIGNATURES_KEY)
|
||||||
|
val fingerprints = mutableSetOf<String>()
|
||||||
|
for (j in 0 until signaturesJson.length()) {
|
||||||
|
if (filterUserDebug) {
|
||||||
|
if (USER_DEBUG_KEY == signaturesJson.getJSONObject(j)
|
||||||
|
.optString(BUILD_KEY) && USER_BUILD_TYPE != Build.TYPE
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fingerprints.add(signaturesJson.getJSONObject(j).getString(FINGERPRINT_KEY))
|
||||||
|
}
|
||||||
|
return AndroidPrivilegedApp(
|
||||||
|
packageName = appInfoJsonObject.getString(PACKAGE_NAME_KEY),
|
||||||
|
fingerprints = fingerprints
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JSONObject from a list of AndroidPrivilegedApp objects.
|
||||||
|
* The structure will be similar to what `extractPrivilegedApps` expects.
|
||||||
|
*
|
||||||
|
* @param privilegedApps The list of AndroidPrivilegedApp objects.
|
||||||
|
* @return A JSONObject representing the list.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun toJsonObject(privilegedApps: List<AndroidPrivilegedApp>): JSONObject {
|
||||||
|
val rootJsonObject = JSONObject()
|
||||||
|
val appsJsonArray = JSONArray()
|
||||||
|
|
||||||
|
for (app in privilegedApps) {
|
||||||
|
val appInfoObject = JSONObject()
|
||||||
|
appInfoObject.put(PACKAGE_NAME_KEY, app.packageName)
|
||||||
|
|
||||||
|
val signaturesArray = JSONArray()
|
||||||
|
for (fingerprint in app.fingerprints) {
|
||||||
|
val signatureObject = JSONObject()
|
||||||
|
signatureObject.put(FINGERPRINT_KEY, fingerprint)
|
||||||
|
// If needed: signatureObject.put(BUILD_KEY, "user")
|
||||||
|
signaturesArray.put(signatureObject)
|
||||||
|
}
|
||||||
|
appInfoObject.put(SIGNATURES_KEY, signaturesArray)
|
||||||
|
|
||||||
|
val appContainerObject = JSONObject()
|
||||||
|
appContainerObject.put(TYPE_KEY, ANDROID_TYPE_KEY)
|
||||||
|
appContainerObject.put(APP_INFO_KEY, appInfoObject)
|
||||||
|
|
||||||
|
appsJsonArray.put(appContainerObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootJsonObject.put(APPS_KEY, appsJsonArray)
|
||||||
|
return rootJsonObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.AssetManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
@@ -42,7 +42,6 @@ import androidx.credentials.provider.ProviderGetCredentialRequest
|
|||||||
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
import com.kunzisoft.encrypt.Signature
|
import com.kunzisoft.encrypt.Signature
|
||||||
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
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
|
||||||
@@ -54,6 +53,7 @@ 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.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
|
||||||
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.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
@@ -328,61 +328,20 @@ object PasskeyHelper {
|
|||||||
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the origin from a privileged allow list,
|
|
||||||
* [fileName] is the name of the file in assets containing the origin list as JSON
|
|
||||||
*/
|
|
||||||
private fun getOriginFromPrivilegedAllowListFile(
|
|
||||||
callingAppInfo: CallingAppInfo,
|
|
||||||
assets: AssetManager,
|
|
||||||
fileName: String
|
|
||||||
): String? {
|
|
||||||
val privilegedAllowList = assets.open(fileName).bufferedReader().use {
|
|
||||||
it.readText()
|
|
||||||
}
|
|
||||||
return callingAppInfo.getOrigin(privilegedAllowList)?.removeSuffix("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the origin from the predefined privileged allow lists
|
|
||||||
*/
|
|
||||||
private fun getOriginFromPrivilegedAllowLists(
|
|
||||||
callingAppInfo: CallingAppInfo,
|
|
||||||
assets: AssetManager
|
|
||||||
): String? {
|
|
||||||
return try {
|
|
||||||
// TODO add the manual privileged apps
|
|
||||||
// Check the community apps first
|
|
||||||
getOriginFromPrivilegedAllowListFile(
|
|
||||||
callingAppInfo = callingAppInfo,
|
|
||||||
assets = assets,
|
|
||||||
fileName = FILE_NAME_PRIVILEGED_APPS_COMMUNITY
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Then the Google list if allowed
|
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
|
||||||
// http://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
|
|
||||||
getOriginFromPrivilegedAllowListFile(
|
|
||||||
callingAppInfo = callingAppInfo,
|
|
||||||
assets = assets,
|
|
||||||
fileName = FILE_NAME_PRIVILEGED_APPS_GOOGLE
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to retrieve the origin asynchronously,
|
* Utility method to retrieve the origin asynchronously,
|
||||||
* checks for the presence of the application in the privilege list of the passkeys_privileged_apps_google.json file,
|
* checks for the presence of the application in the privilege lists
|
||||||
|
*
|
||||||
|
* @param providedClientDataHash Client data hash precalculated by the system
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param context Context for file operations.
|
||||||
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
|
* 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
|
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
|
||||||
*/
|
*/
|
||||||
suspend fun getOrigin(
|
suspend fun getOrigin(
|
||||||
providedClientDataHash: ByteArray?,
|
providedClientDataHash: ByteArray?,
|
||||||
callingAppInfo: CallingAppInfo?,
|
callingAppInfo: CallingAppInfo?,
|
||||||
assets: AssetManager,
|
context: Context,
|
||||||
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||||
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
|
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -392,7 +351,7 @@ object PasskeyHelper {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
// For trusted browsers like Chrome and Firefox
|
// For trusted browsers like Chrome and Firefox
|
||||||
val callOrigin = getOriginFromPrivilegedAllowLists(callingAppInfo, assets)
|
val callOrigin = getOriginFromPrivilegedAllowLists(callingAppInfo, context)
|
||||||
|
|
||||||
// Build the default Android origin
|
// Build the default Android origin
|
||||||
val androidOrigin = AndroidOrigin(
|
val androidOrigin = AndroidOrigin(
|
||||||
@@ -436,12 +395,12 @@ object PasskeyHelper {
|
|||||||
/**
|
/**
|
||||||
* Utility method to create a passkey and the associated creation request parameters
|
* Utility method to create a passkey and the associated creation 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
|
* [context] context to manage package verification files
|
||||||
* [passkeyCreated] is called asynchronously when the passkey has been created
|
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||||
*/
|
*/
|
||||||
suspend fun retrievePasskeyCreationRequestParameters(
|
suspend fun retrievePasskeyCreationRequestParameters(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
assetManager: AssetManager,
|
context: Context,
|
||||||
passkeyCreated: (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
passkeyCreated: (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
@@ -476,7 +435,7 @@ object PasskeyHelper {
|
|||||||
getOrigin(
|
getOrigin(
|
||||||
providedClientDataHash = clientDataHash,
|
providedClientDataHash = clientDataHash,
|
||||||
callingAppInfo = callingAppInfo,
|
callingAppInfo = callingAppInfo,
|
||||||
assets = assetManager,
|
context = context,
|
||||||
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
||||||
passkeyCreated.invoke(
|
passkeyCreated.invoke(
|
||||||
passkey,
|
passkey,
|
||||||
@@ -546,12 +505,12 @@ 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
|
* [context] context to manage package verification files
|
||||||
* [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,
|
context: Context,
|
||||||
result: (PublicKeyCredentialUsageParameters) -> Unit
|
result: (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
@@ -566,7 +525,7 @@ object PasskeyHelper {
|
|||||||
getOrigin(
|
getOrigin(
|
||||||
providedClientDataHash = clientDataHash,
|
providedClientDataHash = clientDataHash,
|
||||||
callingAppInfo = callingAppInfo,
|
callingAppInfo = callingAppInfo,
|
||||||
assets = assetManager,
|
context = context,
|
||||||
onOriginRetrieved = { appOrigin, clientDataHash ->
|
onOriginRetrieved = { appOrigin, clientDataHash ->
|
||||||
result.invoke(
|
result.invoke(
|
||||||
PublicKeyCredentialUsageParameters(
|
PublicKeyCredentialUsageParameters(
|
||||||
@@ -642,7 +601,4 @@ object PasskeyHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val BACKUP_ELIGIBILITY = true
|
private const val BACKUP_ELIGIBILITY = true
|
||||||
|
|
||||||
private const val FILE_NAME_PRIVILEGED_APPS_COMMUNITY = "passkeys_privileged_apps_community.json"
|
|
||||||
private const val FILE_NAME_PRIVILEGED_APPS_GOOGLE = "passkeys_privileged_apps_google.json"
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* 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.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object PrivilegedAllowLists {
|
||||||
|
|
||||||
|
private const val FILE_NAME_PRIVILEGED_APPS_CUSTOM = "passkeys_privileged_apps_custom.json"
|
||||||
|
private const val FILE_NAME_PRIVILEGED_APPS_COMMUNITY = "passkeys_privileged_apps_community.json"
|
||||||
|
private const val FILE_NAME_PRIVILEGED_APPS_GOOGLE = "passkeys_privileged_apps_google.json"
|
||||||
|
|
||||||
|
private fun retrieveContentFromStream(
|
||||||
|
inputStream: InputStream,
|
||||||
|
): String {
|
||||||
|
return inputStream.use { fileInputStream ->
|
||||||
|
fileInputStream.bufferedReader(Charsets.UTF_8).readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the origin from a predefined privileged allow list
|
||||||
|
*
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param inputStream File input stream containing the origin list as JSON
|
||||||
|
*/
|
||||||
|
private fun getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
inputStream: InputStream
|
||||||
|
): String? {
|
||||||
|
val privilegedAllowList = retrieveContentFromStream(inputStream)
|
||||||
|
return callingAppInfo.getOrigin(privilegedAllowList)?.removeSuffix("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the origin from the predefined privileged allow lists
|
||||||
|
*
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param context Context for file operations.
|
||||||
|
*/
|
||||||
|
fun getOriginFromPrivilegedAllowLists(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
context: Context
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
try {
|
||||||
|
// Check the custom apps first
|
||||||
|
getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
|
||||||
|
.inputStream()
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Then the community apps list
|
||||||
|
getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Then the Google list if allowed
|
||||||
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
|
// http://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
|
||||||
|
getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of predefined AndroidPrivilegedApp objects from an asset JSON file.
|
||||||
|
*
|
||||||
|
* @param inputStream File input stream containing the origin list as JSON
|
||||||
|
*/
|
||||||
|
private fun retrievePrivilegedApps(
|
||||||
|
inputStream: InputStream
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
val jsonObject = JSONObject(retrieveContentFromStream(inputStream))
|
||||||
|
return AndroidPrivilegedApp.extractPrivilegedApps(jsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of predefined AndroidPrivilegedApp objects from a context
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
*/
|
||||||
|
fun retrievePredefinedPrivilegedApps(
|
||||||
|
context: Context
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
return try {
|
||||||
|
val predefinedApps = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)))
|
||||||
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
|
predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)))
|
||||||
|
}
|
||||||
|
predefinedApps
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(PrivilegedAllowLists::class.simpleName, "Error retrieving privileged apps", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of AndroidPrivilegedApp objects from the custom JSON file.
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
*/
|
||||||
|
fun retrieveCustomPrivilegedApps(
|
||||||
|
context: Context
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
return try {
|
||||||
|
retrievePrivilegedApps(File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM).inputStream())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i(PrivilegedAllowLists::class.simpleName, "No custom privileged apps", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of all predefined and custom AndroidPrivilegedApp objects.
|
||||||
|
*/
|
||||||
|
fun retrieveAllPrivilegedApps(
|
||||||
|
context: Context
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
return retrievePredefinedPrivilegedApps(context) + retrieveCustomPrivilegedApps(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a list of custom AndroidPrivilegedApp objects to a JSON file.
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
* @param privilegedApps The list of apps to save.
|
||||||
|
* @return True if saving was successful, false otherwise.
|
||||||
|
*/
|
||||||
|
fun saveCustomPrivilegedApps(context: Context, privilegedApps: List<AndroidPrivilegedApp>): Boolean {
|
||||||
|
return try {
|
||||||
|
val jsonToSave = AndroidPrivilegedApp.toJsonObject(privilegedApps)
|
||||||
|
val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
|
||||||
|
|
||||||
|
// Delete existing file before writing to ensure atomicity if needed
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
file.outputStream().use { fileOutputStream ->
|
||||||
|
fileOutputStream.write(
|
||||||
|
jsonToSave
|
||||||
|
.toString(4) // toString(4) for pretty print
|
||||||
|
.toByteArray(Charsets.UTF_8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(PrivilegedAllowLists::class.simpleName, "Error saving privileged apps", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the custom JSON file.
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
* @return True if deletion was successful or file didn't exist, false otherwise.
|
||||||
|
*/
|
||||||
|
fun deletePrivilegedAppsFile(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
} else {
|
||||||
|
true // File didn't exist, so considered "successfully deleted"
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e(PrivilegedAllowLists::class.simpleName, "Error deleting privileged apps file", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,39 +23,55 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.AndroidOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListSelectionItemAdapter
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListSelectionItemAdapter
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.getInstalledBrowsersWithSignatures
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.viewmodel.PasskeysPrivilegedAppsViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
class PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
|
class PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
|
||||||
: InputPreferenceDialogFragmentCompat() {
|
: InputPreferenceDialogFragmentCompat() {
|
||||||
private var mAdapter = ListSelectionItemAdapter<AndroidOrigin>()
|
|
||||||
private var mListBrowsers: List<AndroidOrigin> = mutableListOf()
|
private var mAdapter = ListSelectionItemAdapter<AndroidPrivilegedApp>()
|
||||||
|
private val passkeysPrivilegedAppsViewModel : PasskeysPrivilegedAppsViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mListBrowsers = getInstalledBrowsersWithSignatures(requireContext())
|
|
||||||
// TODO filter with current privileged apps
|
lifecycleScope.launch {
|
||||||
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
passkeysPrivilegedAppsViewModel.retrievePrivilegedAppsToSelect()
|
||||||
|
|
||||||
|
passkeysPrivilegedAppsViewModel.uiState.collect { uiState ->
|
||||||
|
when(uiState) {
|
||||||
|
is PasskeysPrivilegedAppsViewModel.UiState.Loading -> {}
|
||||||
|
is PasskeysPrivilegedAppsViewModel.UiState.OnPrivilegedAppsToSelectRetrieved -> {
|
||||||
|
mAdapter.setItems(uiState.privilegedApps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindDialogView(view: View) {
|
override fun onBindDialogView(view: View) {
|
||||||
super.onBindDialogView(view)
|
super.onBindDialogView(view)
|
||||||
view.findViewById<RecyclerView>(R.id.pref_dialog_list).apply {
|
view.findViewById<RecyclerView>(R.id.pref_dialog_list).apply {
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = LinearLayoutManager(context)
|
||||||
adapter = mAdapter.apply {
|
adapter = mAdapter
|
||||||
setItems(mListBrowsers)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogClosed(positiveResult: Boolean) {
|
override fun onDialogClosed(positiveResult: Boolean) {
|
||||||
if (positiveResult) {
|
if (positiveResult) {
|
||||||
// TODO Save selected item in JSON
|
passkeysPrivilegedAppsViewModel.saveSelectedPrivilegedApp(mAdapter.selectedItem)
|
||||||
mAdapter.selectedItem
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.kunzisoft.keepass.settings.preferencedialogfragment.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.deletePrivilegedAppsFile
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.retrieveAllPrivilegedApps
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.getInstalledBrowsersWithSignatures
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeysPrivilegedAppsViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
|
val uiState: StateFlow<UiState> = _uiState
|
||||||
|
|
||||||
|
fun retrievePrivilegedAppsToSelect() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val privilegedApps = retrieveAllPrivilegedApps(getApplication())
|
||||||
|
val privilegedAppsToSelect = getInstalledBrowsersWithSignatures(getApplication())
|
||||||
|
.filter {
|
||||||
|
privilegedApps.none { privilegedApp ->
|
||||||
|
privilegedApp.packageName == it.packageName
|
||||||
|
&& privilegedApp.fingerprints.any {
|
||||||
|
fingerprint -> fingerprint in it.fingerprints
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiState.value = UiState.OnPrivilegedAppsToSelectRetrieved(privilegedAppsToSelect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSelectedPrivilegedApp(privilegedApp: AndroidPrivilegedApp?) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
privilegedApp?.let {
|
||||||
|
saveCustomPrivilegedApps(getApplication(), listOf(privilegedApp))
|
||||||
|
} ?: deletePrivilegedAppsFile(getApplication())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class UiState {
|
||||||
|
|
||||||
|
object Loading : UiState()
|
||||||
|
data class OnPrivilegedAppsToSelectRetrieved(val privilegedApps: List<AndroidPrivilegedApp>) : UiState()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,11 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
import com.kunzisoft.encrypt.Signature.getAllFingerprints
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.model.AndroidOrigin
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -103,9 +103,9 @@ object AppUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidOrigin> {
|
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
val browserList = mutableListOf<AndroidOrigin>()
|
val browserList = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
|
||||||
// Create a generic web intent
|
// Create a generic web intent
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
@@ -132,9 +132,9 @@ object AppUtil {
|
|||||||
packageName,
|
packageName,
|
||||||
PackageManager.GET_SIGNING_CERTIFICATES
|
PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
)
|
)
|
||||||
val signatureFingerprints = packageInfo.signingInfo.getApplicationFingerprints()
|
val signatureFingerprints = packageInfo.signingInfo.getAllFingerprints()
|
||||||
signatureFingerprints?.let {
|
signatureFingerprints?.let {
|
||||||
browserList.add(AndroidOrigin(packageName, signatureFingerprints))
|
browserList.add(AndroidPrivilegedApp(packageName, signatureFingerprints))
|
||||||
processedPackageNames.add(packageName)
|
processedPackageNames.add(packageName)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pref_dialog_list_text"
|
android:id="@+id/pref_dialog_list_text"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
style="@style/KeepassDXStyle.Title.Entry"
|
style="@style/KeepassDXStyle.SubTitle.Entry"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Cali-95 modified by 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.encrypt
|
package com.kunzisoft.encrypt
|
||||||
|
|
||||||
import android.content.pm.SigningInfo
|
import android.content.pm.SigningInfo
|
||||||
@@ -272,32 +291,29 @@ object Signature {
|
|||||||
/**
|
/**
|
||||||
* Retrieves all relevant SHA-256 signature fingerprints for a given package.
|
* Retrieves all relevant SHA-256 signature fingerprints for a given package.
|
||||||
*
|
*
|
||||||
* @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 getAllFingerprints(signingInfo: SigningInfo?): List<String>? {
|
fun SigningInfo.getAllFingerprints(): Set<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")
|
||||||
val signatures = mutableSetOf<String>()
|
val signatures = mutableSetOf<String>()
|
||||||
if (signingInfo != null) {
|
// Includes past and current keys if rotation occurred. This is generally preferred.
|
||||||
// Includes past and current keys if rotation occurred. This is generally preferred.
|
signingCertificateHistory?.forEach { signature ->
|
||||||
signingInfo.signingCertificateHistory?.forEach { signature ->
|
signatureToSha256Fingerprint(signature)?.let { signatures.add(it) }
|
||||||
|
}
|
||||||
|
// If only one signer and history is empty (e.g. new app), this might be needed.
|
||||||
|
// Or if multiple signers are explicitly used for the APK content.
|
||||||
|
if (hasMultipleSigners()) {
|
||||||
|
apkContentsSigners?.forEach { signature ->
|
||||||
signatureToSha256Fingerprint(signature)?.let { signatures.add(it) }
|
signatureToSha256Fingerprint(signature)?.let { signatures.add(it) }
|
||||||
}
|
}
|
||||||
// If only one signer and history is empty (e.g. new app), this might be needed.
|
} else { // Fallback for single signer if history was somehow null/empty
|
||||||
// Or if multiple signers are explicitly used for the APK content.
|
signingCertificateHistory?.firstOrNull()?.let {
|
||||||
if (signingInfo.hasMultipleSigners()) {
|
signatureToSha256Fingerprint(it)?.let { fp -> signatures.add(fp) }
|
||||||
signingInfo.apkContentsSigners?.forEach { signature ->
|
|
||||||
signatureToSha256Fingerprint(signature)?.let { signatures.add(it) }
|
|
||||||
}
|
|
||||||
} else { // Fallback for single signer if history was somehow null/empty
|
|
||||||
signingInfo.signingCertificateHistory?.firstOrNull()?.let {
|
|
||||||
signatureToSha256Fingerprint(it)?.let { fp -> signatures.add(fp) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (signatures.isEmpty()) null else signatures.toList()
|
return if (signatures.isEmpty()) null else signatures
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(Signature::class.java.simpleName, "Error getting signatures", e)
|
Log.e(Signature::class.java.simpleName, "Error getting signatures", e)
|
||||||
return null
|
return null
|
||||||
@@ -305,17 +321,24 @@ object Signature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines a list of signature into a single string for database storage.
|
* Combines a list of signatures into a single string for database storage.
|
||||||
*
|
*
|
||||||
* @return A single string with fingerprints joined by a ##SIG## delimiter,
|
* @return A single string with fingerprints joined by a ##SIG## delimiter,
|
||||||
* or null if the input list is null or empty.
|
* or null if the input list is null or empty.
|
||||||
*/
|
*/
|
||||||
fun SigningInfo.getApplicationFingerprints(): String? {
|
fun SigningInfo.getApplicationFingerprints(): String? {
|
||||||
val fingerprints = getAllFingerprints(this)
|
val fingerprints = getAllFingerprints()
|
||||||
if (fingerprints.isNullOrEmpty()) {
|
if (fingerprints.isNullOrEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return fingerprints.joinToString(SIGNATURE_DELIMITER)
|
return fingerprints.singleLineFingerprints()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines a set of signatures into a single string for database storage.
|
||||||
|
*/
|
||||||
|
fun Set<String>.singleLineFingerprints(): String? {
|
||||||
|
return this.joinToString(SIGNATURE_DELIMITER)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import com.kunzisoft.encrypt.Signature.fingerprintToUrlSafeBase64
|
|||||||
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
|
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an Android app origin by a list of [AndroidOrigin] and a list of [WebOrigin].
|
||||||
|
* If at least one [AndroidOrigin] is verified, the [verified] flag is set to true.
|
||||||
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppOrigin(
|
data class AppOrigin(
|
||||||
val verified: Boolean,
|
val verified: Boolean,
|
||||||
@@ -96,6 +100,10 @@ data class AppOrigin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an Android app origin, the [packageName] is the applicationId of the app
|
||||||
|
* and the [fingerprint] is the
|
||||||
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AndroidOrigin(
|
data class AndroidOrigin(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user