mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Small refactoring and add doc
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user