diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index 509cece30..e408607e0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -41,14 +41,12 @@ 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.OriginHelper import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode 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.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationComponent import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters import com.kunzisoft.keepass.database.ContextualDatabase @@ -115,7 +113,6 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { PendingIntentHandler.setCreateCredentialResponse( intent = responseIntent, response = buildCreatePublicKeyCredentialResponse( - packageName = packageName, publicKeyCredentialCreationParameters = it ) ) @@ -183,7 +180,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { finish() } ?: run { Log.e(TAG, "Unable to auto select passkey, usage parameters are empty") - setResult(Activity.RESULT_CANCELED) + setResult(RESULT_CANCELED) finish() } } @@ -244,7 +241,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { finish() } ?: run { Log.e(TAG, "Unable to auto select passkey, usage parameters are empty") - setResult(Activity.RESULT_CANCELED) + setResult(RESULT_CANCELED) finish() } } @@ -254,67 +251,62 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { searchInfo: SearchInfo ) { Log.d(TAG, "Launch passkey registration") - PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)?.callingAppInfo?.let { callingAppInfo -> - retrievePasskeyCreationRequestParameters( - creationOptions = intent.retrievePasskeyCreationComponent(), - webOrigin = OriginHelper.getWebOrigin(callingAppInfo, assets), - apkSigningCertificate = - callingAppInfo - .signingInfo.apkContentsSigners - .getOrNull(0)?.toByteArray(), - passkeyCreated = { passkey, publicKeyCredentialParameters -> - // Save the requested parameters - mPasskey = passkey - mCreationParameters = publicKeyCredentialParameters - // Manage the passkey and create a register info - val registerInfo = RegisterInfo( + retrievePasskeyCreationRequestParameters( + intent = intent, + assetManager = assets, + packageName = packageName, + passkeyCreated = { passkey, publicKeyCredentialParameters -> + // Save the requested parameters + mPasskey = passkey + mCreationParameters = publicKeyCredentialParameters + // Manage the passkey and create a register info + val registerInfo = RegisterInfo( + searchInfo = searchInfo, + username = null, + passkey = passkey + ) + // If nodeId already provided + intent.retrieveNodeId()?.let { nodeId -> + autoRegisterPasskeyAndSetResult(database, nodeId) + } ?: run { + SearchHelper.checkAutoSearchInfo( + context = this, + database = database, searchInfo = searchInfo, - username = null, - passkey = passkey + onItemsFound = { openedDatabase, _ -> + Log.w(TAG, "Passkey found for registration, " + + "but launch manual registration for a new entry") + GroupActivity.launchForRegistration( + context = this, + activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, + database = openedDatabase, + registerInfo = registerInfo, + typeMode = TypeMode.PASSKEY + ) + }, + onItemNotFound = { openedDatabase -> + Log.d(TAG, "Launch new manual registration in opened database") + GroupActivity.launchForRegistration( + context = this, + activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, + database = openedDatabase, + registerInfo = registerInfo, + typeMode = TypeMode.PASSKEY + ) + }, + onDatabaseClosed = { + Log.d(TAG, "Manual passkey registration in closed database") + FileDatabaseSelectActivity.launchForRegistration( + context = this, + activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, + registerInfo = registerInfo, + typeMode = TypeMode.PASSKEY + ) + } ) - // If nodeId already provided - intent.retrieveNodeId()?.let { nodeId -> - autoRegisterPasskeyAndSetResult(database, nodeId) - } ?: run { - SearchHelper.checkAutoSearchInfo( - context = this, - database = database, - searchInfo = searchInfo, - onItemsFound = { openedDatabase, _ -> - Log.w(TAG, "Passkey found for registration, " + - "but launch manual registration for a new entry") - GroupActivity.launchForRegistration( - context = this, - activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, - database = openedDatabase, - registerInfo = registerInfo, - typeMode = TypeMode.PASSKEY - ) - }, - onItemNotFound = { openedDatabase -> - Log.d(TAG, "Launch new manual registration in opened database") - GroupActivity.launchForRegistration( - context = this, - activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, - database = openedDatabase, - registerInfo = registerInfo, - typeMode = TypeMode.PASSKEY - ) - }, - onDatabaseClosed = { - Log.d(TAG, "Manual passkey registration in closed database") - FileDatabaseSelectActivity.launchForRegistration( - context = this, - activityResultLauncher = mPasskeyRegistrationActivityResultLauncher, - registerInfo = registerInfo, - typeMode = TypeMode.PASSKEY - ) - } - ) - } } - ) - } + } + ) } companion object { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt index 034d366ab..96312864c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt @@ -46,7 +46,8 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity -import com.kunzisoft.keepass.credentialprovider.passkey.util.JsonHelper +import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions +import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.helper.SearchHelper @@ -117,13 +118,11 @@ class PasskeyProviderService : CredentialProviderService() { val passkeyEntries: MutableList = mutableListOf() - val relyingPartyJson = JsonHelper - .parseJsonToRequestOptions(option.requestJson) - .relyingParty + val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId val searchInfo = SearchInfo().apply { - relyingParty = relyingPartyJson + relyingParty = relyingPartyId } - Log.d(TAG, "Build passkey search for relying party $relyingPartyJson") + Log.d(TAG, "Build passkey search for relying party $relyingPartyId") SearchHelper.checkAutoSearchInfo( context = this, database = mDatabase, @@ -154,7 +153,7 @@ class PasskeyProviderService : CredentialProviderService() { } }, onItemNotFound = { _ -> - Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyJson") + Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") Log.d(TAG, "Add pending intent for passkey selection in opened database") PasskeyLauncherActivity.getPendingIntent( context = applicationContext, @@ -252,11 +251,11 @@ class PasskeyProviderService : CredentialProviderService() { val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username) val createEntries: MutableList = mutableListOf() + val relyingPartyId = PublicKeyCredentialCreationOptions(request.requestJson).relyingPartyEntity.id val searchInfo = SearchInfo().apply { - relyingParty = JsonHelper - .parseJsonToCreateOptions(request.requestJson) - .relyingParty + relyingParty = relyingPartyId } + Log.d(TAG, "Build passkey search for relying party $relyingPartyId") SearchHelper.checkAutoSearchInfo( context = this, database = mDatabase, diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt new file mode 100644 index 000000000..7ea51bced --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt @@ -0,0 +1,66 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import androidx.credentials.exceptions.GetCredentialUnknownException +import com.kunzisoft.asymmetric.Signature +import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import org.json.JSONObject + +class AuthenticatorAssertionResponse( + private val requestOptions: PublicKeyCredentialRequestOptions, + private val userPresent: Boolean, + private val userVerified: Boolean, + private val backupEligibility: Boolean, + private val backupState: Boolean, + private var userHandle: String, + privateKey: String, + private val clientDataResponse: ClientDataResponse, +) : AuthenticatorResponse { + + override var clientJson = JSONObject() + private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData( + relyingPartyId = requestOptions.rpId.toByteArray(), + userPresent = userPresent, + userVerified = userVerified, + backupEligibility = backupEligibility, + backupState = backupState + ) + private var signature: ByteArray = byteArrayOf() + + init { + signature = Signature.sign(privateKey, dataToSign()) + ?: throw GetCredentialUnknownException("signing failed") + } + + private fun dataToSign(): ByteArray { + return authenticatorData + clientDataResponse.hashData() + } + + override fun json(): JSONObject { + // https://www.w3.org/TR/webauthn-3/#authdata-flags + return clientJson.apply { + put("clientDataJSON", clientDataResponse.buildResponse()) + put("authenticatorData", b64Encode(authenticatorData)) + put("signature", b64Encode(signature)) + put("userHandle", userHandle) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt new file mode 100644 index 000000000..d44a0c116 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt @@ -0,0 +1,86 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import org.json.JSONArray +import org.json.JSONObject + +class AuthenticatorAttestationResponse( + private val requestOptions: PublicKeyCredentialCreationOptions, + private val credentialId: ByteArray, + private val credentialPublicKey: ByteArray, + private val userPresent: Boolean, + private val userVerified: Boolean, + private val backupEligibility: Boolean, + private val backupState: Boolean, + private val publicKeyTypeId: Long, + private val publicKeyCbor: ByteArray, + private val clientDataResponse: ClientDataResponse, +) : AuthenticatorResponse { + + override var clientJson = JSONObject() + var attestationObject: ByteArray + + init { + attestationObject = defaultAttestationObject() + } + + private fun buildAuthData(): ByteArray { + return AuthenticatorData.buildAuthenticatorData( + relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(), + userPresent = userPresent, + userVerified = userVerified, + backupEligibility = backupEligibility, + backupState = backupState, + attestedCredentialData = true + ) + AAGUID + + //credIdLen + byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) + + credentialId + + credentialPublicKey + } + + internal fun defaultAttestationObject(): ByteArray { + val ao = mutableMapOf() + ao.put("fmt", "none") + ao.put("attStmt", emptyMap()) + ao.put("authData", buildAuthData()) + return Cbor().encode(ao) + } + + override fun json(): JSONObject { + // See AuthenticatorAttestationResponseJSON at + // https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson + return clientJson.apply { + put("clientDataJSON", clientDataResponse.buildResponse()) + put("authenticatorData", b64Encode(buildAuthData())) + put("transports", JSONArray(listOf("internal", "hybrid"))) + put("publicKey", b64Encode(publicKeyCbor)) + put("publicKeyAlgorithm", publicKeyTypeId) + put("attestationObject", b64Encode(attestationObject)) + } + } + + companion object { + // TODO Authenticator Attestation Global Unique Identifier + private val AAGUID = ByteArray(16) { 0 } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt new file mode 100644 index 000000000..6df2b10be --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt @@ -0,0 +1,57 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import com.kunzisoft.encrypt.HashManager + +class AuthenticatorData { + + companion object { + fun buildAuthenticatorData( + relyingPartyId: ByteArray, + userPresent: Boolean, + userVerified: Boolean, + backupEligibility: Boolean, + backupState: Boolean, + attestedCredentialData: Boolean = false + ): ByteArray { + // https://www.w3.org/TR/webauthn-3/#table-authData + var flags = 0 + if (userPresent) + flags = flags or 0x01 + // bit at index 1 is reserved + if (userVerified) + flags = flags or 0x04 + if (backupEligibility) + flags = flags or 0x08 + if (backupState) + flags = flags or 0x10 + // bit at index 5 is reserved + if (attestedCredentialData) { + flags = flags or 0x40 + } + // bit at index 7: Extension data included == false + + return HashManager.hashSha256(relyingPartyId) + + byteArrayOf(flags.toByte()) + + byteArrayOf(0, 0, 0, 0) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt new file mode 100644 index 000000000..57d9ab2dc --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt @@ -0,0 +1,28 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import org.json.JSONObject + +interface AuthenticatorResponse { + var clientJson: JSONObject + + fun json(): JSONObject +} diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt new file mode 100644 index 000000000..8dbbb8657 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY) +class Cbor { + data class Item(val item: Any, val len: Int) + + data class Arg(val arg: Long, val len: Int) + + val TYPE_UNSIGNED_INT = 0x00 + val TYPE_NEGATIVE_INT = 0x01 + val TYPE_BYTE_STRING = 0x02 + val TYPE_TEXT_STRING = 0x03 + val TYPE_ARRAY = 0x04 + val TYPE_MAP = 0x05 + val TYPE_TAG = 0x06 + val TYPE_FLOAT = 0x07 + + fun decode(data: ByteArray): Any { + val ret = parseItem(data, 0) + return ret.item + } + + fun encode(data: Any): ByteArray { + if (data is Number) { + if (data is Double) { + throw IllegalArgumentException("Don't support doubles yet") + } else { + val value = data.toLong() + if (value >= 0) { + return createArg(TYPE_UNSIGNED_INT, value) + } else { + return createArg(TYPE_NEGATIVE_INT, -1 - value) + } + } + } + if (data is ByteArray) { + return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data + } + if (data is String) { + return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray() + } + if (data is List<*>) { + var ret = createArg(TYPE_ARRAY, data.size.toLong()) + for (i in data) { + ret += encode(i!!) + } + return ret + } + if (data is Map<*, *>) { + // See: + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form + var ret = createArg(TYPE_MAP, data.size.toLong()) + var byteMap: MutableMap = mutableMapOf() + for (i in data) { + // Convert to byte arrays so we can sort them. + byteMap.put(encode(i.key!!), encode(i.value!!)) + } + + var keysList = ArrayList(byteMap.keys) + keysList.sortedWith( + Comparator { a, b -> + // If two keys have different lengths, the shorter one sorts earlier; + // If two keys have the same length, the one with the lower value in (byte-wise) + // lexical order sorts earlier. + var aBytes = byteMap.get(a)!! + var bBytes = byteMap.get(b)!! + when { + a.size > b.size -> 1 + a.size < b.size -> -1 + aBytes.size > bBytes.size -> 1 + aBytes.size < bBytes.size -> -1 + else -> 0 + } + } + ) + + for (key in keysList) { + ret += key + ret += byteMap.get(key)!! + } + return ret + } + throw IllegalArgumentException("Bad type") + } + + private fun getType(data: ByteArray, offset: Int): Int { + val d = data[offset].toInt() + return (d and 0xFF) shr 5 + } + + private fun getArg(data: ByteArray, offset: Int): Arg { + val arg = data[offset].toLong() and 0x1F + if (arg < 24) { + return Arg(arg, 1) + } + if (arg == 24L) { + return Arg(data[offset + 1].toLong() and 0xFF, 2) + } + if (arg == 25L) { + var ret = (data[offset + 1].toLong() and 0xFF) shl 8 + ret = ret or (data[offset + 2].toLong() and 0xFF) + return Arg(ret, 3) + } + if (arg == 26L) { + var ret = (data[offset + 1].toLong() and 0xFF) shl 24 + ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16) + ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8) + ret = ret or (data[offset + 4].toLong() and 0xFF) + return Arg(ret, 5) + } + throw IllegalArgumentException("Bad arg") + } + + private fun parseItem(data: ByteArray, offset: Int): Item { + val itemType = getType(data, offset) + val arg = getArg(data, offset) + println("Type $itemType ${arg.arg} ${arg.len}") + + when (itemType) { + TYPE_UNSIGNED_INT -> { + return Item(arg.arg, arg.len) + } + TYPE_NEGATIVE_INT -> { + return Item(-1 - arg.arg, arg.len) + } + TYPE_BYTE_STRING -> { + val ret = + data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt()) + return Item(ret, arg.len + arg.arg.toInt()) + } + TYPE_TEXT_STRING -> { + val ret = + data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt()) + return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt()) + } + TYPE_ARRAY -> { + val ret = mutableListOf() + var consumed = arg.len + for (i in 0 until arg.arg.toInt()) { + val item = parseItem(data, offset + consumed) + ret.add(item.item) + consumed += item.len + } + return Item(ret.toList(), consumed) + } + TYPE_MAP -> { + val ret = mutableMapOf() + var consumed = arg.len + for (i in 0 until arg.arg.toInt()) { + val key = parseItem(data, offset + consumed) + consumed += key.len + val value = parseItem(data, offset + consumed) + consumed += value.len + ret[key.item] = value.item + } + return Item(ret.toMap(), consumed) + } + else -> { + throw IllegalArgumentException("Bad type") + } + } + } + + private fun createArg(type: Int, arg: Long): ByteArray { + val t = type shl 5 + val a = arg.toInt() + if (arg < 24) { + return byteArrayOf(((t or a) and 0xFF).toByte()) + } + if (arg <= 0xFF) { + return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte()) + } + if (arg <= 0xFFFF) { + return byteArrayOf( + ((t or 25) and 0xFF).toByte(), + ((a shr 8) and 0xFF).toByte(), + (a and 0xFF).toByte() + ) + } + if (arg <= 0xFFFFFFFF) { + return byteArrayOf( + ((t or 26) and 0xFF).toByte(), + ((a shr 24) and 0xFF).toByte(), + ((a shr 16) and 0xFF).toByte(), + ((a shr 8) and 0xFF).toByte(), + (a and 0xFF).toByte() + ) + } + throw IllegalArgumentException("bad Arg") + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt new file mode 100644 index 000000000..ae323b904 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt @@ -0,0 +1,37 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +open class ClientDataDefinedResponse( + private val clientDataHash: ByteArray +): ClientDataResponse { + + override fun hashData(): ByteArray { + return clientDataHash + } + + override fun buildResponse(): String { + return CLIENT_DATA_JSON_PRIVILEGED + } + + companion object { + private const val CLIENT_DATA_JSON_PRIVILEGED = "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataNotDefinedResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataNotDefinedResponse.kt new file mode 100644 index 000000000..d7b9bc4f3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataNotDefinedResponse.kt @@ -0,0 +1,67 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import com.kunzisoft.encrypt.HashManager +import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import org.json.JSONObject + +open class ClientDataNotDefinedResponse( + type: Type, + challenge: ByteArray, + origin: String, + crossOrigin: Boolean? = null, + topOrigin: String? = null, + packageName: String? +): AuthenticatorResponse, ClientDataResponse { + override var clientJson = JSONObject() + + init { + // https://w3c.github.io/webauthn/#client-data + clientJson.put("type", type.value) + clientJson.put("challenge", b64Encode(challenge)) + clientJson.put("origin", origin) + crossOrigin?.let { + clientJson.put("crossOrigin", it) + } + topOrigin?.let { + clientJson.put("topOrigin", it) + } + packageName?.let { + clientJson.put("androidPackageName", packageName) + } + } + + override fun json(): JSONObject { + return clientJson + } + + enum class Type(val value: String) { + GET("webauthn.get"), CREATE("webauthn.create") + } + + override fun buildResponse(): String { + return b64Encode(json().toString().toByteArray()) + } + + override fun hashData(): ByteArray { + return HashManager.hashSha256(json().toString().toByteArray()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt new file mode 100644 index 000000000..bdc5b3ec2 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt @@ -0,0 +1,25 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +interface ClientDataResponse { + fun hashData(): ByteArray + fun buildResponse(): String +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt new file mode 100644 index 000000000..2f128d1d3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kunzisoft.keepass.credentialprovider.passkey.data + +data class PublicKeyCredentialRpEntity(val name: String, val id: String) + +data class PublicKeyCredentialUserEntity( + val name: String, + val id: ByteArray, + val displayName: String +) + +data class PublicKeyCredentialParameters(val type: String, val alg: Long) + +data class PublicKeyCredentialDescriptor( + val type: String, + val id: ByteArray, + val transports: List +) + +data class AuthenticatorSelectionCriteria( + val authenticatorAttachment: String, + val residentKey: String, + val requireResidentKey: Boolean = false, + val userVerification: String = "preferred" +) diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt new file mode 100644 index 000000000..9fa66a8a8 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt @@ -0,0 +1,50 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.data + +import org.json.JSONObject + +class FidoPublicKeyCredential( + val id: String, + val response: AuthenticatorResponse, + val authenticatorAttachment: String +) { + + fun json(): String { + // see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension + val discoverableCredential = true + val rk = JSONObject() + rk.put("rk", discoverableCredential) + val credProps = JSONObject() + credProps.put("credProps", rk) + + // See RegistrationResponseJSON at + // https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson + val ret = JSONObject() + ret.put("id", id) + ret.put("rawId", id) + ret.put("type", "public-key") + ret.put("authenticatorAttachment", authenticatorAttachment) + ret.put("response", response.json()) + ret.put("clientExtensionResults", JSONObject()) // TODO credProps + + return ret.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt index a5f9fcc81..e58e37a7d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt @@ -1,9 +1,72 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.kunzisoft.keepass.credentialprovider.passkey.data -data class PublicKeyCredentialCreationOptions( - val relyingParty: String, - val challenge: ByteArray, // TODO Equals Hashcode - val username: String, - val userId: ByteArray, // TODO Equals Hashcode - val keyTypeIdList: List -) \ No newline at end of file +import android.util.Log +import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper +import org.json.JSONObject + +class PublicKeyCredentialCreationOptions(requestJson: String) { + val json: JSONObject = JSONObject(requestJson) + + val relyingPartyEntity: PublicKeyCredentialRpEntity + val userEntity: PublicKeyCredentialUserEntity + val challenge: ByteArray + val pubKeyCredParams: List + + var timeout: Long + var excludeCredentials: List + var authenticatorSelection: AuthenticatorSelectionCriteria + var attestation: String + + init { + val rpJson = json.getJSONObject("rp") + relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id")) + val rpUser = json.getJSONObject("user") + val userId = Base64Helper.b64Decode(rpUser.getString("id")) + userEntity = + PublicKeyCredentialUserEntity( + rpUser.getString("name"), + userId, + rpUser.getString("displayName") + ) + challenge = Base64Helper.b64Decode(json.getString("challenge")) + val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams") + val pubKeyCredParamsTmp: MutableList = mutableListOf() + for (i in 0 until pubKeyCredParamsJson.length()) { + val e = pubKeyCredParamsJson.getJSONObject(i) + pubKeyCredParamsTmp.add( + PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg")) + ) + } + pubKeyCredParams = pubKeyCredParamsTmp.toList() + + timeout = json.optLong("timeout", 0) + // TODO: Fix excludeCredentials and authenticatorSelection + excludeCredentials = emptyList() + authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required") + attestation = json.optString("attestation", "none") + + Log.i("WebAuthn", "Challenge $challenge()") + Log.i("WebAuthn", "rp $relyingPartyEntity") + Log.i("WebAuthn", "user $userEntity") + Log.i("WebAuthn", "pubKeyCredParams $pubKeyCredParams") + Log.i("WebAuthn", "timeout $timeout") + Log.i("WebAuthn", "excludeCredentials $excludeCredentials") + Log.i("WebAuthn", "authenticatorSelection $authenticatorSelection") + Log.i("WebAuthn", "attestation $attestation") + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt index a92030ad0..304fba835 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt @@ -1,11 +1,29 @@ +/* + * 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 . + * + */ package com.kunzisoft.keepass.credentialprovider.passkey.data import java.security.KeyPair data class PublicKeyCredentialCreationParameters( - val relyingParty: String, + val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions, val credentialId: ByteArray, // TODO Equals Hashcode val signatureKey: Pair, - val isPrivilegedApp: Boolean, - val challenge: ByteArray, // TODO Equals Hashcode + val clientDataResponse: ClientDataResponse ) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt index 686a7f866..a2cf97906 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt @@ -1,6 +1,31 @@ +/* + * 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 . + * + */ package com.kunzisoft.keepass.credentialprovider.passkey.data -data class PublicKeyCredentialRequestOptions( - val relyingParty: String, - val challengeString: String -) \ No newline at end of file +import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper +import org.json.JSONObject + +class PublicKeyCredentialRequestOptions(requestJson: String) { + val json: JSONObject = JSONObject(requestJson) + val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge")) + val timeout: Long = json.optLong("timeout", 0) + val rpId: String = json.optString("rpId", "") + val userVerification: String = json.optString("userVerification", "preferred") +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt index e67a42578..16b01a345 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt @@ -1,9 +1,25 @@ +/* + * 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 . + * + */ package com.kunzisoft.keepass.credentialprovider.passkey.data data class PublicKeyCredentialUsageParameters( - val relyingParty: String, - val packageName: String? = null, - val clientDataHash: ByteArray?, // TODO Equals Hashcode - val isPrivilegedApp: Boolean, - val challenge: ByteArray, // TODO Equals Hashcode + val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions, + val clientDataResponse: ClientDataResponse ) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt index fd83a3f2f..faebbfa40 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt @@ -1,19 +1,41 @@ +/* + * 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 . + * + */ package com.kunzisoft.keepass.credentialprovider.passkey.util -import org.apache.commons.codec.binary.Base64 +import android.util.Base64 class Base64Helper { companion object { - fun b64Decode(encodedString: String?): ByteArray { - return Base64.decodeBase64(encodedString) + 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 android.util.Base64.encodeToString( + return Base64.encodeToString( data, - android.util.Base64.NO_PADDING or android.util.Base64.NO_WRAP or android.util.Base64.URL_SAFE + Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE ) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/JsonHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/JsonHelper.kt deleted file mode 100644 index e080d32b5..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/JsonHelper.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.kunzisoft.keepass.credentialprovider.passkey.util - -import android.annotation.SuppressLint -import androidx.credentials.webauthn.Cbor -import com.kunzisoft.encrypt.HashManager -import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions -import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions -import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Decode -import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode -import org.json.JSONArray -import org.json.JSONObject - -class JsonHelper { - - companion object { - - fun generateClientDataJsonNonPrivileged( - challenge: ByteArray, - origin: String, - packageName: String?, - isGet: Boolean, - isCrossOriginAdded: Boolean - ): String { - val clientJson = JSONObject() - val type = if (isGet) { - "webauthn.get" - } else { - "webauthn.create" - } - clientJson.put("type", type) - clientJson.put("challenge", b64Encode(challenge)) - clientJson.put("origin", origin) - - if (isCrossOriginAdded) { - clientJson.put("crossOrigin", false) - } - - if (packageName != null) { - clientJson.put("androidPackageName", packageName) - } - - val clientDataFinal = clientJson.toString().replace("\\/", "/") - return clientDataFinal - } - - fun generateClientDataJsonPrivileged(): String { - // will be replaced by the clientData from the privileged app like a browser - return "" - } - - fun generateAuthDataForUsage( - rpId: ByteArray, - userPresent: Boolean, - userVerified: Boolean, - backupEligibility: Boolean, - backupState: Boolean, - attestedCredentialData: Boolean = false - ): ByteArray { - val rpHash = HashManager.hashSha256(rpId) - - // see https://www.w3.org/TR/webauthn-3/#table-authData - var flags = 0 - val one = 1 - if (userPresent) { - flags = flags or one.shl(0) - } - // bit at index 1 is reserved - - if (userVerified) { - flags = flags or one.shl(2) - } - if (backupEligibility) { - flags = flags or one.shl(3) - } - if (backupState) { - flags = flags or one.shl(4) - } - - // bit at index 5 is reserved - - if (attestedCredentialData) { - flags = flags or one.shl(6) - } - - // bit at index 7: Extension data included == false - - val signCount = byteArrayOf(0, 0, 0, 0) - - return rpHash + byteArrayOf(flags.toByte()) + signCount - } - - fun generateAuthDataForCreate( - rpId: ByteArray, - userPresent: Boolean, - userVerified: Boolean, - backupEligibility: Boolean, - backupState: Boolean, - credentialId: ByteArray, - credentialPublicKey: ByteArray - ): ByteArray { - val authDataPartOne = generateAuthDataForUsage( - rpId, - userPresent, - userVerified, - backupEligibility, - backupState, - attestedCredentialData = true - ) - - // Authenticator Attestation Globally Unique Identifier - val aaguid = ByteArray(16) { 0 } - - val credIdLen = - byteArrayOf((credentialId.size.shr(8)).toByte(), credentialId.size.toByte()) - - return authDataPartOne + aaguid + credIdLen + credentialId + credentialPublicKey - } - - fun generateDataTosSignNonPrivileged( - clientDataJson: String, - authenticatorData: ByteArray - ): ByteArray { - val hash = HashManager.hashSha256(clientDataJson.toByteArray()) - return authenticatorData + hash - } - - fun generateDataToSignPrivileged( - clientDataHash: ByteArray, - authenticatorData: ByteArray - ): ByteArray { - return authenticatorData + clientDataHash - } - - fun generateAttestationObject(authData: ByteArray): ByteArray { - val ao = mutableMapOf() - ao["fmt"] = "none" - ao["attStmt"] = emptyMap() - ao["authData"] = authData - return generateCborFromMap(ao) - } - - @SuppressLint("RestrictedApi") - fun generateCborFromMap(map: Map): ByteArray { - return Cbor().encode(map) - } - - fun createAuthenticatorAttestationResponseJSON( - credentialId: ByteArray, - clientDataJson: String, - attestationObject: ByteArray, - publicKeyCbor: ByteArray, - authData: ByteArray, - publicKeyTypeId: Long - ): String { - // See AuthenticatorAttestationResponseJSON at - // https://www.w3.org/TR/webauthn-3/#ref-for-dom-publickeycredential-tojson - - val rk = JSONObject() - - // see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension - val discoverableCredential = true - rk.put("rk", discoverableCredential) - val credProps = JSONObject() - credProps.put("credProps", rk) - - - val response = JSONObject() - response.put("attestationObject", b64Encode(attestationObject)) - response.put("clientDataJSON", clientDataJson) - response.put("transports", JSONArray(listOf("internal", "hybrid"))) - response.put("publicKeyAlgorithm", publicKeyTypeId) - response.put("publicKey", b64Encode(publicKeyCbor)) - response.put("authenticatorData", b64Encode(authData)) - - val all = JSONObject() - all.put("id", b64Encode(credentialId)) - all.put("rawId", b64Encode(credentialId)) - all.put("response", response) - all.put("type", "public-key") - all.put("clientExtensionResults", credProps) - all.put("authenticatorAttachment", "platform") - return all.toString() - } - - fun generateGetCredentialResponse( - clientDataJson: ByteArray, - authenticatorData: ByteArray, - signature: ByteArray, - userHandle: String, - id: String - ): String { - - val response = JSONObject() - response.put("clientDataJSON", b64Encode(clientDataJson)) - response.put("authenticatorData", b64Encode(authenticatorData)) - response.put("signature", b64Encode(signature)) - response.put("userHandle", userHandle) - - val ret = JSONObject() - ret.put("id", id) - ret.put("rawId", id) - ret.put("type", "public-key") - ret.put("authenticatorAttachment", "platform") - ret.put("response", response) - ret.put("clientExtensionResults", JSONObject()) - - return ret.toString() - } - - fun parseJsonToRequestOptions(requestJson: String): PublicKeyCredentialRequestOptions { - val jsonObject = JSONObject(requestJson) - - val challengeString = jsonObject.getString("challenge") - val relyingParty = jsonObject.optString("rpId", "") - - return PublicKeyCredentialRequestOptions(relyingParty, challengeString) - } - - fun parseJsonToCreateOptions(requestJson: String): PublicKeyCredentialCreationOptions { - val jsonObject = JSONObject(requestJson) - val rpJson = jsonObject.getJSONObject("rp") - val relyingParty = rpJson.getString("id") - - val challenge = b64Decode(jsonObject.getString("challenge")) - - val rpUser = jsonObject.getJSONObject("user") - val username = rpUser.getString("name") - val userId = b64Decode(rpUser.getString("id")) - - - val pubKeyCredParamsJson = jsonObject.getJSONArray("pubKeyCredParams") - val keyTypeIdList: MutableList = mutableListOf() - for (i in 0 until pubKeyCredParamsJson.length()) { - val e = pubKeyCredParamsJson.getJSONObject(i) - if (e.getString("type") == "public-key") { - keyTypeIdList.add(e.getLong("alg")) - } - } - - return PublicKeyCredentialCreationOptions( - relyingParty, - challenge, - username, - userId, - keyTypeIdList.distinct() - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginHelper.kt deleted file mode 100644 index 6d1895964..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginHelper.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.kunzisoft.keepass.credentialprovider.passkey.util - -import android.content.res.AssetManager -import androidx.credentials.provider.CallingAppInfo - -class OriginHelper { - - companion object { - - const val DEFAULT_PROTOCOL = "https://" - - fun getWebOrigin(callingAppInfo: CallingAppInfo?, assets: AssetManager): String? { - val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use { - it.readText() - } - // for trusted browsers like Chrome and Firefox - return callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/") - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt new file mode 100644 index 000000000..e743f9eac --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt @@ -0,0 +1,80 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.credentialprovider.passkey.util + +import android.content.res.AssetManager +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.credentials.provider.CallingAppInfo + +@RequiresApi(Build.VERSION_CODES.P) +class OriginManager( + callingAppInfo: CallingAppInfo?, + assets: AssetManager, + private val relyingParty: String +) { + private val webOrigin: String? + private val apkSigningCertificate: ByteArray? = callingAppInfo?.signingInfo?.apkContentsSigners + ?.getOrNull(0)?.toByteArray() + + init { + val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use { + it.readText() + } + // for trusted browsers like Chrome and Firefox + webOrigin = callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/") + } + + private fun isPrivilegedApp(): Boolean { + return webOrigin != null + && webOrigin == (DEFAULT_PROTOCOL + relyingParty) + } + + // TODO isPrivileged app + fun checkPrivilegedApp( + clientDataHash: ByteArray? + ) { + val isPrivilegedApp = isPrivilegedApp() && clientDataHash != null + Log.d(TAG, "isPrivilegedApp = $isPrivilegedApp") + if (!isPrivilegedApp) { + AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate) + } + } + + fun checkPrivilegedApp() { + val isPrivilegedApp = isPrivilegedApp() + Log.d(TAG, "isPrivilegedApp = $isPrivilegedApp") + if (!isPrivilegedApp) { + AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate) + } + } + + val origin: String + get() { + return webOrigin ?: (DEFAULT_PROTOCOL + relyingParty) + } + + companion object { + private val TAG = OriginManager::class.simpleName + const val DEFAULT_PROTOCOL = "https://" + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index 6ff17147d..fa3ea0ce1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.util import android.app.Activity import android.content.Context import android.content.Intent +import android.content.res.AssetManager import android.os.Build import android.os.Bundle import android.os.ParcelUuid @@ -36,11 +37,21 @@ import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.CreateCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.provider.PendingIntentHandler +import androidx.credentials.provider.ProviderCreateCredentialRequest +import androidx.credentials.provider.ProviderGetCredentialRequest import com.kunzisoft.asymmetric.Signature +import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse +import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse +import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor +import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse +import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataNotDefinedResponse +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.OriginHelper.Companion.DEFAULT_PROTOCOL +import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.DEFAULT_PROTOCOL import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfoPasskey.getPasskey import com.kunzisoft.keepass.model.Passkey @@ -206,20 +217,18 @@ object PasskeyHelper { return chunked(2).map { it.toInt(16).toByte() }.toByteArray() } - fun Intent.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions { - val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this) - ?: throw CreateCredentialUnknownException("could not retrieve request from intent") + fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions { + val request = this if (request.callingRequest !is CreatePublicKeyCredentialRequest) { throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}") } - return JsonHelper.parseJsonToCreateOptions( + return PublicKeyCredentialCreationOptions( (request.callingRequest as CreatePublicKeyCredentialRequest).requestJson ) } - fun Intent.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption { - val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(this) - ?: throw CreateCredentialUnknownException("could not retrieve request from intent") + fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption { + val request = this if (request.credentialOptions.size != 1) { throw GetCredentialUnknownException("not exact one credentialOption") } @@ -230,36 +239,31 @@ object PasskeyHelper { } fun retrievePasskeyCreationRequestParameters( - creationOptions: PublicKeyCredentialCreationOptions, - webOrigin: String?, - apkSigningCertificate: ByteArray?, + intent: Intent, + assetManager: AssetManager, + packageName: String?, passkeyCreated: (Passkey, PublicKeyCredentialCreationParameters) -> Unit ) { - val relyingParty = creationOptions.relyingParty - val username = creationOptions.username - val userHandle = creationOptions.userId - val keyTypeIdList = creationOptions.keyTypeIdList - val challenge = creationOptions.challenge + val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) + val callingAppInfo = getCredentialRequest?.callingAppInfo + val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) + if (createCredentialRequest == null) + throw CreateCredentialUnknownException("could not retrieve request from intent") + val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent() - val isPrivilegedApp = - (webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty) - Log.d(this::class.java.simpleName, "isPrivilegedApp = $isPrivilegedApp") + val relyingParty = creationOptions.relyingPartyEntity.id + val username = creationOptions.userEntity.name + val userHandle = creationOptions.userEntity.id + val pubKeyCredParams = creationOptions.pubKeyCredParams - if (!isPrivilegedApp) { - val isValid = - AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate) - if (!isValid) { - throw CreateCredentialUnknownException( - "could not verify relation between app " + - "and relyingParty $relyingParty" - ) - } - } + val originManager = OriginManager(callingAppInfo, assetManager, relyingParty) + originManager.checkPrivilegedApp() val credentialId = KeePassDXRandom.generateCredentialId() - val (keyPair, keyTypeId) = Signature.generateKeyPair(keyTypeIdList) - ?: throw CreateCredentialUnknownException("no known public key type found") + val (keyPair, keyTypeId) = Signature.generateKeyPair( + pubKeyCredParams.map { params -> params.alg } + ) ?: throw CreateCredentialUnknownException("no known public key type found") val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private) // create new entry in database @@ -268,68 +272,51 @@ object PasskeyHelper { username = username, displayName = "$relyingParty (Passkey)", privateKeyPem = privateKeyPem, - credentialId = Base64Helper.b64Encode(credentialId), - userHandle = Base64Helper.b64Encode(userHandle), + credentialId = b64Encode(credentialId), + userHandle = b64Encode(userHandle), relyingParty = DEFAULT_PROTOCOL + relyingParty ), PublicKeyCredentialCreationParameters( - relyingParty = relyingParty, - challenge = challenge, + publicKeyCredentialCreationOptions = creationOptions, credentialId = credentialId, signatureKey = Pair(keyPair, keyTypeId), - isPrivilegedApp = isPrivilegedApp + clientDataResponse = ClientDataNotDefinedResponse( + type = ClientDataNotDefinedResponse.Type.CREATE, + challenge = creationOptions.challenge, + origin = originManager.origin, + packageName = packageName + ) ) ) } fun buildCreatePublicKeyCredentialResponse( - packageName: String?, publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters ): CreatePublicKeyCredentialResponse { val keyPair = publicKeyCredentialCreationParameters.signatureKey.first val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second - - val publicKeyEncoded = Signature.convertPublicKey(keyPair.public, keyTypeId) - val publicKeyMap = Signature.convertPublicKeyToMap(keyPair.public, keyTypeId) - - val authData = JsonHelper.generateAuthDataForCreate( - userPresent = true, - userVerified = true, - backupEligibility = true, - backupState = true, - rpId = publicKeyCredentialCreationParameters.relyingParty.toByteArray(), - credentialId = publicKeyCredentialCreationParameters.credentialId, - credentialPublicKey = JsonHelper.generateCborFromMap(publicKeyMap!!) - ) - - val attestationObject = JsonHelper.generateAttestationObject(authData) - - val clientJson: String - if (publicKeyCredentialCreationParameters.isPrivilegedApp) { - clientJson = JsonHelper.generateClientDataJsonPrivileged() - } else { - val origin = DEFAULT_PROTOCOL + publicKeyCredentialCreationParameters.relyingParty - clientJson = JsonHelper.generateClientDataJsonNonPrivileged( - publicKeyCredentialCreationParameters.challenge, - origin, - packageName, - isCrossOriginAdded = true, - isGet = false - ) - } - - val responseJson = JsonHelper.createAuthenticatorAttestationResponseJSON( - publicKeyCredentialCreationParameters.credentialId, - clientJson, - attestationObject, - publicKeyEncoded!!, - authData, - keyTypeId - ) - + val responseJson = FidoPublicKeyCredential( + id = b64Encode(publicKeyCredentialCreationParameters.credentialId), + response = AuthenticatorAttestationResponse( + requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions, + credentialId = publicKeyCredentialCreationParameters.credentialId, + credentialPublicKey = Cbor().encode(Signature.convertPublicKeyToMap( + publicKeyIn = keyPair.public, + keyTypeId = keyTypeId + ) ?: mapOf()), + userPresent = true, + userVerified = true, + backupEligibility = true, + backupState = true, + publicKeyTypeId = keyTypeId, + publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!, + clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse + ), + authenticatorAttachment = "platform" + ).json() // log only the length to prevent logging sensitive information - Log.d(javaClass.simpleName, "responseJson with length ${responseJson.length} created") + Log.d(javaClass.simpleName, "Json response for key creation") return CreatePublicKeyCredentialResponse(responseJson) } @@ -338,42 +325,32 @@ object PasskeyHelper { intent: Intent, result: (PublicKeyCredentialUsageParameters) -> Unit ) { - val callingAppInfo = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)?.callingAppInfo - val credentialOption = intent.retrievePasskeyUsageComponent() + val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) + if (getCredentialRequest == null) + throw CreateCredentialUnknownException("could not retrieve request from intent") + val callingAppInfo = getCredentialRequest.callingAppInfo + val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent() val clientDataHash = credentialOption.clientDataHash - val requestOptions = JsonHelper.parseJsonToRequestOptions(credentialOption.requestJson) + val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson) + val relyingParty = requestOptions.rpId - val relyingParty = requestOptions.relyingParty - val challenge = Base64Helper.b64Decode(requestOptions.challengeString) - val packageName = callingAppInfo?.packageName - val webOrigin = OriginHelper.getWebOrigin(callingAppInfo, context.assets) - - val isPrivilegedApp = - (webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty && clientDataHash != null) - - Log.d(javaClass.simpleName, "isPrivilegedApp = $isPrivilegedApp") - - if (!isPrivilegedApp) { - if (!AppRelyingPartyRelation.isRelationValid( - relyingParty, - apkSigningCertificate = callingAppInfo?.signingInfo?.apkContentsSigners - ?.getOrNull(0)?.toByteArray() - )) { - throw CreateCredentialUnknownException( - "could not verify relation between app " + - "and relyingParty $relyingParty" - ) - } - } + val originManager = OriginManager(callingAppInfo, context.assets, relyingParty) + originManager.checkPrivilegedApp(clientDataHash) result.invoke( PublicKeyCredentialUsageParameters( - relyingParty = relyingParty, - packageName = packageName, - clientDataHash = clientDataHash, - isPrivilegedApp = isPrivilegedApp, - challenge = challenge + publicKeyCredentialRequestOptions = requestOptions, + clientDataResponse = clientDataHash?.let { + ClientDataDefinedResponse(clientDataHash) + } ?: run { + ClientDataNotDefinedResponse( + type = ClientDataNotDefinedResponse.Type.GET, + challenge = requestOptions.challenge, + origin = originManager.origin, + packageName = callingAppInfo.packageName + ) + } ) ) } @@ -382,46 +359,21 @@ object PasskeyHelper { usageParameters: PublicKeyCredentialUsageParameters, passkey: Passkey ): PublicKeyCredential { - - // https://www.w3.org/TR/webauthn-3/#authdata-flags - val authenticatorData = JsonHelper.generateAuthDataForUsage( - usageParameters.relyingParty.toByteArray(), - userPresent = true, - userVerified = true, - backupEligibility = true, - backupState = true - ) - - val clientDataJson: String - val dataToSign: ByteArray - if (usageParameters.isPrivilegedApp) { - clientDataJson = JsonHelper.generateClientDataJsonPrivileged() - dataToSign = - JsonHelper.generateDataToSignPrivileged(usageParameters.clientDataHash!!, authenticatorData) - } else { - val origin = DEFAULT_PROTOCOL + usageParameters.relyingParty - clientDataJson = JsonHelper.generateClientDataJsonNonPrivileged( - usageParameters.challenge, - origin, - usageParameters.packageName, - isGet = true, - isCrossOriginAdded = false - ) - dataToSign = - JsonHelper.generateDataTosSignNonPrivileged(clientDataJson, authenticatorData) - } - - val signature = Signature.sign(passkey.privateKeyPem, dataToSign) - ?: throw GetCredentialUnknownException("signing failed") - - val getCredentialResponse = - JsonHelper.generateGetCredentialResponse( - clientDataJson.toByteArray(), - authenticatorData, - signature, - passkey.userHandle, - passkey.credentialId - ) + val getCredentialResponse = FidoPublicKeyCredential( + id = passkey.credentialId, + response = AuthenticatorAssertionResponse( + requestOptions = usageParameters.publicKeyCredentialRequestOptions, + userPresent = true, + userVerified = true, + backupEligibility = true, + backupState = true, + userHandle = passkey.userHandle, + privateKey = passkey.privateKeyPem, + clientDataResponse = usageParameters.clientDataResponse + ), + authenticatorAttachment = "platform" + ).json() + Log.d(javaClass.simpleName, "Json response for key usage") return PublicKeyCredential(getCredentialResponse) } diff --git a/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt b/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt index e311cabb2..f1aaee828 100644 --- a/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt +++ b/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt @@ -23,9 +23,7 @@ import java.security.spec.ECGenParameterSpec object Signature { // see at https://www.iana.org/assignments/cose/cose.xhtml - const val ES256_ALGORITHM: Long = -7 - const val RS256_ALGORITHM: Long = -257 private const val RS256_KEY_SIZE_IN_BITS = 2048 @@ -171,6 +169,4 @@ object Signature { Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found") return null } - - } \ No newline at end of file diff --git a/crypto/src/main/java/com/kunzisoft/random/KeePassDXRandom.kt b/crypto/src/main/java/com/kunzisoft/random/KeePassDXRandom.kt index 7a54efe0d..98dda7162 100644 --- a/crypto/src/main/java/com/kunzisoft/random/KeePassDXRandom.kt +++ b/crypto/src/main/java/com/kunzisoft/random/KeePassDXRandom.kt @@ -1,3 +1,22 @@ +/* + * 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 . + * + */ package com.kunzisoft.random import java.security.SecureRandom @@ -15,7 +34,5 @@ class KeePassDXRandom { internalSecureRandom.nextBytes(credentialId) return credentialId } - } - } \ No newline at end of file