fix: Small refactoring and add doc

This commit is contained in:
J-Jamet
2025-08-29 12:23:44 +02:00
parent 98007c962d
commit 4f10d13691
4 changed files with 172 additions and 54 deletions

View File

@@ -41,7 +41,6 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.checkInAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
@@ -49,6 +48,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSe
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedClientDataResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
@@ -92,35 +92,25 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
passkey?.let {
mUsageParameters?.let { usageParameters ->
// Check verified origin
if (usageParameters.androidAppVerified) {
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
usageParameters = usageParameters,
passkey = passkey
)
)
)
} else {
usageParameters.androidApp.checkInAppOrigin(
appOrigin = appOrigin,
onOriginChecked = {
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
usageParameters = usageParameters,
passkey = passkey
)
getVerifiedClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin,
onOriginChecked = { clientDataResponse ->
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = clientDataResponse,
passkey = passkey
)
)
},
onOriginNotChecked = {
throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}")
}
)
}
)
},
onOriginNotChecked = {
throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}")
}
)
} ?: run {
throw IOException("Usage parameters is null")
}
@@ -215,14 +205,15 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
?.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
?.passkey
?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found")
?: throw GetCredentialUnknownException("No passkey with nodeId $nodeId found")
val result = Intent()
PendingIntentHandler.setGetCredentialResponse(
result,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
usageParameters = usageParameters,
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = usageParameters.clientDataResponse,
passkey = passkey
)
)
@@ -243,7 +234,11 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
appOrigin: AppOrigin?
) {
Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(intent, assets, appOrigin) { usageParameters ->
retrievePasskeyUsageRequestParameters(
intent = intent,
assetManager = assets,
appOrigin = appOrigin
) { usageParameters ->
// Save the requested parameters
mUsageParameters = usageParameters
// Manage the passkey to use

View File

@@ -27,7 +27,7 @@ open class ClientDataBuildResponse(
type: Type,
challenge: ByteArray,
origin: String,
crossOrigin: Boolean? = null,
crossOrigin: Boolean? = false,
topOrigin: String? = null,
): AuthenticatorResponse, ClientDataResponse {
override var clientJson = JSONObject()

View File

@@ -30,6 +30,9 @@ import com.kunzisoft.keepass.model.AppOrigin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Utility class to manage the origin of credential provider applications
*/
@RequiresApi(Build.VERSION_CODES.P)
class OriginManager(
private val providedClientDataHash: ByteArray?,
@@ -37,6 +40,11 @@ class OriginManager(
private val assets: AssetManager
) {
/**
* Retrieves the Android origin to be stored in the database,
* call [onOriginRetrieved] if the origin is already calculated by the system
* call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
*/
suspend fun getOriginAtCreation(
onOriginRetrieved: (appInfoToStore: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit
@@ -62,9 +70,9 @@ class OriginManager(
}
/**
* Retrieve the Android origin from an [AppOrigin],
* call [onOriginRetrieved] if the origin is already calculated by the system
* call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
* Retrieves the origin to verify usage,
* calls [onOriginRetrieved] if the origin is already calculated by the system
* calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
*/
suspend fun getOriginAtUsage(
appOrigin: AppOrigin?,
@@ -91,6 +99,12 @@ class OriginManager(
)
}
/**
* Utility method to retrieve the origin asynchronously,
* checks for the presence of the application in the privilege list of the trustedPackages.json file,
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list
* call [onOriginNotRetrieved] if the origin is not retrieved from the system
*/
private suspend fun getOrigin(
onOriginRetrieved: (appInfoRetrieved: AppIdentifier, origin: String, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (appInfoRetrieved: AppIdentifier) -> Unit
@@ -113,9 +127,9 @@ class OriginManager(
withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) {
Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo")
onOriginRetrieved(appIdentifier, callOrigin, providedClientDataHash)
onOriginRetrieved(appIdentifier, callOrigin, providedClientDataHash)
} else {
onOriginNotRetrieved(appIdentifier)
onOriginNotRetrieved(appIdentifier)
}
}
}

View File

@@ -44,12 +44,14 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttest
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.checkInAppOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey
@@ -65,6 +67,11 @@ import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey
/**
* Utility class to manage the passkey elements,
* allows to add and retrieve intent values with preconfigured keys,
* and makes it easy to create creation and usage requests
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
object PasskeyHelper {
@@ -119,6 +126,9 @@ object PasskeyHelper {
}
}
/**
* Add an authentication code generated by an entry to the intent
*/
fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
putExtras(Bundle().apply {
val timestamp = Instant.now().epochSecond
@@ -132,48 +142,78 @@ object PasskeyHelper {
})
}
/**
* Retrieve the passkey from the intent
*/
fun Intent.retrievePasskey(): Passkey? {
return this.getParcelableExtraCompat(EXTRA_PASSKEY)
}
/**
* Remove the passkey from the intent
*/
fun Intent.removePasskey() {
return this.removeExtra(EXTRA_PASSKEY)
}
/**
* Add the search info to the intent
*/
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
searchInfo?.let {
putExtra(EXTRA_SEARCH_INFO, searchInfo)
}
}
/**
* Retrieve the search info from the intent
*/
fun Intent.retrieveSearchInfo(): SearchInfo? {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
}
/**
* Add the app origin to the intent
*/
fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
appOrigin?.let {
putExtra(EXTRA_APP_ORIGIN, appOrigin)
}
}
/**
* Retrieve the app origin from the intent
*/
fun Intent.retrieveAppOrigin(): AppOrigin? {
return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
}
/**
* Remove the app origin from the intent
*/
fun Intent.removeAppOrigin() {
return this.removeExtra(EXTRA_APP_ORIGIN)
}
/**
* Add the node id to the intent, useful for auto passkey selection
*/
fun Intent.addNodeId(nodeId: UUID?) {
nodeId?.let {
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
}
}
/**
* Retrieve the node id from the intent
*/
fun Intent.retrieveNodeId(): UUID? {
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
}
/**
* Check the timestamp and authentication code transmitted via PendingIntent
*/
fun checkSecurity(intent: Intent, nodeId: UUID?) {
val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
if (timestampString.isNullOrEmpty())
@@ -192,12 +232,9 @@ object PasskeyHelper {
)
}
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
return generateAuthenticationCode(
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
)
}
/**
* Verify the authentication code from the encrypted message received from the intent
*/
private fun verifyAuthenticationCode(
valueToCheck: String?,
authenticationCode: ByteArray
@@ -210,6 +247,18 @@ object PasskeyHelper {
throw CreateCredentialUnknownException("Authentication code incorrect")
}
/**
* Generate the authentication code base on the entry [nodeId] and [timestamp]
*/
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
return generateAuthenticationCode(
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
)
}
/**
* Generate the authentication code base on the entry [message]
*/
private fun generateAuthenticationCode(message: String): ByteArray {
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
keyStore.load(null)
@@ -226,6 +275,9 @@ object PasskeyHelper {
return authenticationCode
}
/**
* Generate the HMAC key if cannot be found in the KeyStore
*/
private fun generateKey(): SecretKey? {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
@@ -240,6 +292,9 @@ object PasskeyHelper {
return key
}
/**
* Retrieve the [PublicKeyCredentialCreationOptions] from the intent
*/
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
val request = this
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
@@ -252,6 +307,9 @@ object PasskeyHelper {
)
}
/**
* Retrieve the [GetPublicKeyCredentialOption] from the intent
*/
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
val request = this
if (request.credentialOptions.size != 1) {
@@ -263,6 +321,12 @@ object PasskeyHelper {
return request.credentialOptions[0] as GetPublicKeyCredentialOption
}
/**
* Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request
* [assetManager] has been transferred to the origin manager to manage package verification files
* [passkeyCreated] is called asynchronously when the passkey has been created
*/
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
assetManager: AssetManager,
@@ -325,8 +389,7 @@ object PasskeyHelper {
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.CREATE,
challenge = creationOptions.challenge,
origin = origin,
crossOrigin = false // TODO should always be false?
origin = origin
)
)
)
@@ -334,6 +397,10 @@ object PasskeyHelper {
)
}
/**
* Build the passkey public key credential response,
* by calling this method the user is always recognized as present and verified
*/
fun buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
): CreatePublicKeyCredentialResponse {
@@ -351,8 +418,8 @@ object PasskeyHelper {
) ?: mapOf<Int, Any>()),
userPresent = true,
userVerified = true,
backupEligibility = false, // TODO should always be false?
backupState = false, // TODO should always be false?
backupEligibility = false,
backupState = false,
publicKeyTypeId = keyTypeId,
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
@@ -364,6 +431,13 @@ object PasskeyHelper {
return CreatePublicKeyCredentialResponse(responseJson)
}
/**
* Utility method to use a passkey and create the associated usage request parameters
* [intent] allows to retrieve the request
* [assetManager] has been transferred to the origin manager to manage package verification files
* [appOrigin] retrieves the origin params stored in an entry, which may be null if not found or if the database is closed.
* [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
*/
suspend fun retrievePasskeyUsageRequestParameters(
intent: Intent,
assetManager: AssetManager,
@@ -402,8 +476,7 @@ object PasskeyHelper {
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = requestOptions.challenge,
origin = origin,
crossOrigin = false // TODO should always be false?
origin = origin
),
androidApp = appIdentifier,
androidAppVerified = verified
@@ -413,21 +486,26 @@ object PasskeyHelper {
)
}
/**
* Build the passkey public key credential response,
* by calling this method the user is always recognized as present and verified
*/
fun buildPasskeyPublicKeyCredential(
usageParameters: PublicKeyCredentialUsageParameters,
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
response = AuthenticatorAssertionResponse(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = false, // TODO should always be false?
backupState = false, // TODO should always be false?
backupEligibility = false,
backupState = false,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
clientDataResponse = usageParameters.clientDataResponse
clientDataResponse = clientDataResponse
),
authenticatorAttachment = "platform"
).json()
@@ -435,4 +513,35 @@ object PasskeyHelper {
return PublicKeyCredential(getCredentialResponse)
}
/**
* Verify that the application signature is contained in the [appOrigin]
* or that the webDomain contains the origin
*/
fun getVerifiedClientDataResponse(
usageParameters: PublicKeyCredentialUsageParameters,
appOrigin: AppOrigin?,
onOriginChecked: (clientDataResponse: ClientDataResponse) -> Unit,
onOriginNotChecked: () -> Unit
) {
if (usageParameters.androidAppVerified) {
onOriginChecked(usageParameters.clientDataResponse)
} else {
usageParameters.androidApp.checkInAppOrigin(
appOrigin = appOrigin,
onOriginChecked = { origin ->
// Origin checked by Android app signature
onOriginChecked(
ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
origin = origin
)
)
},
onOriginNotChecked
)
}
}
}