Merge branch 'feature/passkeys_validation' into feature/Passkeys

This commit is contained in:
J-Jamet
2025-09-02 12:26:30 +02:00
25 changed files with 302 additions and 358 deletions

View File

@@ -107,7 +107,8 @@ android {
} }
packaging { packaging {
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // necessary for bcpkix-jdk18on in crypto // Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
} }
} }

View File

@@ -45,9 +45,8 @@
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="tiramisu" tools:targetApi="s"
android:enableOnBackInvokedCallback="true"> tools:ignore="CredentialDependency">
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" /> android:value="${googleAndroidBackupAPIKey}" />
@@ -198,7 +197,7 @@
android:label="@string/keyboard_setting_label" android:label="@string/keyboard_setting_label"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.credentialprovider.passkey.data package com.kunzisoft.keepass.credentialprovider.passkey.data
import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException
import com.kunzisoft.asymmetric.Signature import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import org.json.JSONObject import org.json.JSONObject

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.credentialprovider.passkey.data package com.kunzisoft.keepass.credentialprovider.passkey.data
import android.util.Log
import com.kunzisoft.encrypt.Base64Helper import com.kunzisoft.encrypt.Base64Helper
import org.json.JSONObject import org.json.JSONObject
@@ -66,15 +65,6 @@ class PublicKeyCredentialCreationOptions(
excludeCredentials = emptyList() excludeCredentials = emptyList()
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required") authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
attestation = json.optString("attestation", "none") attestation = json.optString("attestation", "none")
Log.i(TAG, "challenge $challenge()")
Log.i(TAG, "rp $relyingPartyEntity")
Log.i(TAG, "user $userEntity")
Log.i(TAG, "pubKeyCredParams $pubKeyCredParams")
Log.i(TAG, "timeout $timeout")
Log.i(TAG, "excludeCredentials $excludeCredentials")
Log.i(TAG, "authenticatorSelection $authenticatorSelection")
Log.i(TAG, "attestation $attestation")
} }
companion object { companion object {

View File

@@ -39,9 +39,9 @@ import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -61,11 +61,11 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.WebOrigin import com.kunzisoft.keepass.model.WebOrigin
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.random.KeePassDXRandom
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.security.KeyStore import java.security.KeyStore
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Instant import java.time.Instant
import java.util.UUID import java.util.UUID
import javax.crypto.KeyGenerator import javax.crypto.KeyGenerator
@@ -104,6 +104,8 @@ object PasskeyHelper {
private const val MAX_DIFF_IN_SECONDS = 60 private const val MAX_DIFF_IN_SECONDS = 60
private val internalSecureRandom: SecureRandom = SecureRandom()
/** /**
* Build the Passkey response for one entry * Build the Passkey response for one entry
*/ */
@@ -379,6 +381,17 @@ object PasskeyHelper {
} }
} }
/**
* Generate a credential id randomly
*/
private fun generateCredentialId(): ByteArray {
// see https://w3c.github.io/webauthn/#credential-id
val size = 16
val credentialId = ByteArray(size)
internalSecureRandom.nextBytes(credentialId)
return credentialId
}
/** /**
* Utility method to create a passkey and the associated creation request parameters * Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request * [intent] allows to retrieve the request
@@ -402,7 +415,7 @@ object PasskeyHelper {
val pubKeyCredParams = creationOptions.pubKeyCredParams val pubKeyCredParams = creationOptions.pubKeyCredParams
val clientDataHash = creationOptions.clientDataHash val clientDataHash = creationOptions.clientDataHash
val credentialId = KeePassDXRandom.generateCredentialId() val credentialId = generateCredentialId()
val (keyPair, keyTypeId) = Signature.generateKeyPair( val (keyPair, keyTypeId) = Signature.generateKeyPair(
pubKeyCredParams.map { params -> params.alg } pubKeyCredParams.map { params -> params.alg }
@@ -470,7 +483,8 @@ object PasskeyHelper {
response = AuthenticatorAttestationResponse( response = AuthenticatorAttestationResponse(
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions, requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
credentialId = publicKeyCredentialCreationParameters.credentialId, credentialId = publicKeyCredentialCreationParameters.credentialId,
credentialPublicKey = Cbor().encode(Signature.convertPublicKeyToMap( credentialPublicKey = Cbor().encode(
Signature.convertPublicKeyToMap(
publicKeyIn = keyPair.public, publicKeyIn = keyPair.public,
keyTypeId = keyTypeId keyTypeId = keyTypeId
) ?: mapOf<Int, Any>()), ) ?: mapOf<Int, Any>()),

View File

@@ -29,7 +29,7 @@ import com.kunzisoft.keepass.R
class DialogColorPreference @JvmOverloads constructor(context: Context, class DialogColorPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) { : ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
class DialogListExplanationPreference @JvmOverloads constructor(context: Context, class DialogListExplanationPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
class DurationDialogPreference @JvmOverloads constructor(context: Context, class DurationDialogPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -28,7 +28,7 @@ import java.util.*
class IconPackListPreference @JvmOverloads constructor(context: Context, class IconPackListPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: ListPreference(context, attrs, defStyleAttr, defStyleRes) { : ListPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
open class InputKdfNumberPreference @JvmOverloads constructor(context: Context, open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.utils.DataByte
class InputKdfSizePreference @JvmOverloads constructor(context: Context, class InputKdfSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) { : InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.R
open class InputListPreference @JvmOverloads constructor(context: Context, open class InputListPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.R
open class InputNumberPreference @JvmOverloads constructor(context: Context, open class InputNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.utils.DataByte
open class InputSizePreference @JvmOverloads constructor(context: Context, open class InputSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) { : InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
open class InputTextPreference @JvmOverloads constructor(context: Context, open class InputTextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -1,8 +0,0 @@
package com.kunzisoft.keepass.settings.preference
class PreferenceConstant {
companion object {
const val R_ATTR_DIALOG_PREFERENCE_STYLE: Int = 16842897;
}
}

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
open class TextPreference @JvmOverloads constructor(context: Context, open class TextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible

View File

@@ -38,15 +38,15 @@ android {
} }
packaging { packaging {
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // bouncycastle need this // Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
} }
} }
dependencies { dependencies {
// Crypto // Crypto
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' implementation 'org.bouncycastle:bcpkix-jdk18on:1.81'
androidTestImplementation 'org.testng:testng:6.9.6'
androidTestImplementation "androidx.test:runner:$android_test_version" androidTestImplementation "androidx.test:runner:$android_test_version"
testImplementation "androidx.test:runner:$android_test_version" testImplementation "androidx.test:runner:$android_test_version"
} }

View File

@@ -1,45 +0,0 @@
/*
* Copyright 202 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.encrypt
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
import org.junit.Assert
import org.junit.Test
class AppSignatureTest {
@Test
fun testSingleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
@Test
fun testMultipleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17##SIG##DB:25:8A:A6:19:08:9B:D1:3D:BA:71:9E:5A:DA:EC:FF:7F:12:C8:8F:67:AD:68:3C:1F:BC:F2:28:B3:88:BD:91"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
}

View File

@@ -1,5 +1,7 @@
package com.kunzisoft.asymmetric package com.kunzisoft.encrypt
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
import org.junit.Assert
import org.junit.Test import org.junit.Test
class SignatureTest { class SignatureTest {
@@ -154,4 +156,22 @@ class SignatureTest {
} }
// endregion // endregion
@Test
fun testSingleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
@Test
fun testMultipleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17##SIG##DB:25:8A:A6:19:08:9B:D1:3D:BA:71:9E:5A:DA:EC:FF:7F:12:C8:8F:67:AD:68:3C:1F:BC:F2:28:B3:88:BD:91"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
} }

View File

@@ -1,230 +0,0 @@
package com.kunzisoft.asymmetric
import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
import org.bouncycastle.util.BigIntegers
import org.bouncycastle.util.io.pem.PemWriter
import java.io.StringReader
import java.io.StringWriter
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Security
import java.security.Signature
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
const val ED_DSA_ALGORITHM: Long = -8
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
fun sign(privateKeyPem: String, message: ByteArray): ByteArray? {
val privateKey = createPrivateKey(privateKeyPem)
val algorithmKey = privateKey.algorithm
val algorithmSignature = when (algorithmKey) {
"EC" -> "SHA256withECDSA"
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
"Ed25519" -> "Ed25519"
else -> null
}
if (algorithmSignature == null) {
Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
return null
}
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
sig.initSign(privateKey)
sig.update(message)
return sig.sign()
}
fun createPrivateKey(privateKeyPem: String): PrivateKey {
val targetReader = StringReader(privateKeyPem)
val pemParser = PEMParser(targetReader)
val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo
val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
pemParser.close()
targetReader.close()
return privateKey
}
fun convertPrivateKeyToPem(privateKey: PrivateKey): String {
var useV1Info = false
if (privateKey is BCEdDSAPrivateKey) {
// to generate PEM, which are compatible to KeepassXC
useV1Info = true
}
System.setProperty("org.bouncycastle.pkcs8.v1_info_only", useV1Info.toString().lowercase())
val noOutputEncryption = null
val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption)
val writer = StringWriter()
val pemWriter = PemWriter(writer)
pemWriter.writeObject(pemObjectGenerator)
pemWriter.close()
val privateKeyInPem = writer.toString().trim()
writer.close()
return privateKeyInPem
}
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
for (typeId in keyTypeIdList) {
if (typeId == ES256_ALGORITHM) {
val es256CurveNameBC = "secp256r1"
val spec = ECGenParameterSpec(es256CurveNameBC)
val keyPairGen =
KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(spec)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ES256_ALGORITHM)
} else if (typeId == RS256_ALGORITHM) {
val keyPairGen =
KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(RS256_KEY_SIZE_IN_BITS)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, RS256_ALGORITHM)
} else if (typeId == ED_DSA_ALGORITHM) {
val keyPairGen =
KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ED_DSA_ALGORITHM)
}
}
Log.e(this::class.java.simpleName, "generateKeyPair: no known key type id found")
return null
}
fun convertPublicKey(publicKeyIn: PublicKey, keyTypeId: Long): ByteArray? {
if (keyTypeId == ES256_ALGORITHM) {
if (publicKeyIn is BCECPublicKey) {
publicKeyIn.setPointFormat("UNCOMPRESSED")
return publicKeyIn.encoded
}
} else if (keyTypeId == RS256_ALGORITHM) {
return publicKeyIn.encoded
} else if (keyTypeId == ED_DSA_ALGORITHM) {
return publicKeyIn.encoded
}
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found")
return null
}
fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map<Int, Any>? {
// https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
val keyTypeLabel = 1
val algorithmLabel = 3
if (keyTypeId == ES256_ALGORITHM) {
if (publicKeyIn !is BCECPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ES256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
// constants see at https://w3c.github.io/webauthn/#example-bdbd14cc
val publicKeyMap = mutableMapOf<Int, Any>()
val es256KeyTypeId = 2
val es256EllipticCurveP256Id = 1
publicKeyMap[keyTypeLabel] = es256KeyTypeId
publicKeyMap[algorithmLabel] = ES256_ALGORITHM
publicKeyMap[-1] = es256EllipticCurveP256Id
val ecPoint = publicKeyIn.q
publicKeyMap[-2] = ecPoint.xCoord.encoded
publicKeyMap[-3] = ecPoint.yCoord.encoded
return publicKeyMap
} else if (keyTypeId == RS256_ALGORITHM) {
if (publicKeyIn !is BCRSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $RS256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
// constants see at https://w3c.github.io/webauthn/#example-8dfabc00
val rs256KeySizeInBytes = RS256_KEY_SIZE_IN_BITS / 8
val rs256KeyTypeId = 3
val rs256ExponentSizeInBytes = 3
val publicKeyMap = mutableMapOf<Int, Any>()
publicKeyMap[keyTypeLabel] = rs256KeyTypeId
publicKeyMap[algorithmLabel] = RS256_ALGORITHM
publicKeyMap[-1] =
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
publicKeyMap[-2] =
BigIntegers.asUnsignedByteArray(
rs256ExponentSizeInBytes,
publicKeyIn.publicExponent
)
return publicKeyMap
} else if (keyTypeId == ED_DSA_ALGORITHM) {
if (publicKeyIn !is BCEdDSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ED_DSA_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
val publicKeyMap = mutableMapOf<Int, Any>()
// https://www.rfc-editor.org/rfc/rfc9053#name-key-object-parameters
val octetKeyPairId = 1
val curveLabel = -1
val ed25519CurveId = 6
val publicKeyLabel = -2
publicKeyMap[keyTypeLabel] = octetKeyPairId
publicKeyMap[algorithmLabel] = ED_DSA_ALGORITHM
publicKeyMap[curveLabel] = ed25519CurveId
val length = Ed25519PublicKeyParameters.KEY_SIZE
publicKeyMap[publicKeyLabel] = BigIntegers.asUnsignedByteArray(length,
BigIntegers.fromUnsignedByteArray(publicKeyIn.pointEncoding))
return publicKeyMap
}
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null
}
}

View File

@@ -0,0 +1,237 @@
package com.kunzisoft.encrypt
import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
import org.bouncycastle.util.BigIntegers
import org.bouncycastle.util.io.pem.PemWriter
import java.io.StringReader
import java.io.StringWriter
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Security
import java.security.Signature
import java.security.spec.ECGenParameterSpec
class Signature {
companion object {
// 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
const val ED_DSA_ALGORITHM: Long = -8
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
fun sign(privateKeyPem: String, message: ByteArray): ByteArray? {
val privateKey = createPrivateKey(privateKeyPem)
val algorithmKey = privateKey.algorithm
val algorithmSignature = when (algorithmKey) {
"EC" -> "SHA256withECDSA"
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
"Ed25519" -> "Ed25519"
else -> null
}
if (algorithmSignature == null) {
Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
return null
}
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
sig.initSign(privateKey)
sig.update(message)
return sig.sign()
}
fun createPrivateKey(privateKeyPem: String): PrivateKey {
val targetReader = StringReader(privateKeyPem)
val pemParser = PEMParser(targetReader)
val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo
val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
pemParser.close()
targetReader.close()
return privateKey
}
fun convertPrivateKeyToPem(privateKey: PrivateKey): String {
var useV1Info = false
if (privateKey is BCEdDSAPrivateKey) {
// to generate PEM, which are compatible to KeepassXC
useV1Info = true
}
System.setProperty(
"org.bouncycastle.pkcs8.v1_info_only",
useV1Info.toString().lowercase()
)
val noOutputEncryption = null
val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption)
val writer = StringWriter()
val pemWriter = PemWriter(writer)
pemWriter.writeObject(pemObjectGenerator)
pemWriter.close()
val privateKeyInPem = writer.toString().trim()
writer.close()
return privateKeyInPem
}
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
for (typeId in keyTypeIdList) {
if (typeId == ES256_ALGORITHM) {
val es256CurveNameBC = "secp256r1"
val spec = ECGenParameterSpec(es256CurveNameBC)
val keyPairGen =
KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(spec)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ES256_ALGORITHM)
} else if (typeId == RS256_ALGORITHM) {
val keyPairGen =
KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(RS256_KEY_SIZE_IN_BITS)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, RS256_ALGORITHM)
} else if (typeId == ED_DSA_ALGORITHM) {
val keyPairGen =
KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ED_DSA_ALGORITHM)
}
}
Log.e(this::class.java.simpleName, "generateKeyPair: no known key type id found")
return null
}
fun convertPublicKey(publicKeyIn: PublicKey, keyTypeId: Long): ByteArray? {
if (keyTypeId == ES256_ALGORITHM) {
if (publicKeyIn is BCECPublicKey) {
publicKeyIn.setPointFormat("UNCOMPRESSED")
return publicKeyIn.encoded
}
} else if (keyTypeId == RS256_ALGORITHM) {
return publicKeyIn.encoded
} else if (keyTypeId == ED_DSA_ALGORITHM) {
return publicKeyIn.encoded
}
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found")
return null
}
fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map<Int, Any>? {
// https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
val keyTypeLabel = 1
val algorithmLabel = 3
if (keyTypeId == ES256_ALGORITHM) {
if (publicKeyIn !is BCECPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ES256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
// constants see at https://w3c.github.io/webauthn/#example-bdbd14cc
val publicKeyMap = mutableMapOf<Int, Any>()
val es256KeyTypeId = 2
val es256EllipticCurveP256Id = 1
publicKeyMap[keyTypeLabel] = es256KeyTypeId
publicKeyMap[algorithmLabel] = ES256_ALGORITHM
publicKeyMap[-1] = es256EllipticCurveP256Id
val ecPoint = publicKeyIn.q
publicKeyMap[-2] = ecPoint.xCoord.encoded
publicKeyMap[-3] = ecPoint.yCoord.encoded
return publicKeyMap
} else if (keyTypeId == RS256_ALGORITHM) {
if (publicKeyIn !is BCRSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $RS256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
// constants see at https://w3c.github.io/webauthn/#example-8dfabc00
val rs256KeySizeInBytes = RS256_KEY_SIZE_IN_BITS / 8
val rs256KeyTypeId = 3
val rs256ExponentSizeInBytes = 3
val publicKeyMap = mutableMapOf<Int, Any>()
publicKeyMap[keyTypeLabel] = rs256KeyTypeId
publicKeyMap[algorithmLabel] = RS256_ALGORITHM
publicKeyMap[-1] =
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
publicKeyMap[-2] =
BigIntegers.asUnsignedByteArray(
rs256ExponentSizeInBytes,
publicKeyIn.publicExponent
)
return publicKeyMap
} else if (keyTypeId == ED_DSA_ALGORITHM) {
if (publicKeyIn !is BCEdDSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ED_DSA_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
val publicKeyMap = mutableMapOf<Int, Any>()
// https://www.rfc-editor.org/rfc/rfc9053#name-key-object-parameters
val octetKeyPairId = 1
val curveLabel = -1
val ed25519CurveId = 6
val publicKeyLabel = -2
publicKeyMap[keyTypeLabel] = octetKeyPairId
publicKeyMap[algorithmLabel] = ED_DSA_ALGORITHM
publicKeyMap[curveLabel] = ed25519CurveId
val length = Ed25519PublicKeyParameters.KEY_SIZE
publicKeyMap[publicKeyLabel] = BigIntegers.asUnsignedByteArray(
length,
BigIntegers.fromUnsignedByteArray(publicKeyIn.pointEncoding)
)
return publicKeyMap
}
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null
}
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.random
import java.security.SecureRandom
class KeePassDXRandom {
companion object {
private val internalSecureRandom: SecureRandom = SecureRandom()
fun generateCredentialId(): ByteArray {
// see https://w3c.github.io/webauthn/#credential-id
val size = 16
val credentialId = ByteArray(size)
internalSecureRandom.nextBytes(credentialId)
return credentialId
}
}
}

View File

@@ -33,6 +33,11 @@ android {
includeAndroidResources = true includeAndroidResources = true
} }
} }
packaging {
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
}
} }
dependencies { dependencies {
@@ -42,7 +47,7 @@ dependencies {
implementation 'commons-io:commons-io:2.8.0' implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15' implementation 'commons-codec:commons-codec:1.15'
implementation project(path: ':crypto') api project(path: ':crypto')
// Tests // Tests
androidTestImplementation "androidx.test:runner:$android_test_version" androidTestImplementation "androidx.test:runner:$android_test_version"