implement creation and update of passkeys

This commit is contained in:
cali
2024-10-13 20:37:46 +02:00
parent 69114c3cc0
commit c907750446
29 changed files with 2060 additions and 807 deletions

View File

@@ -113,20 +113,29 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## Credential Provider
Use this version only for testing at your own risk.
Use this version only for testing at your own risk. Make a backup of your databases.
### requirements
- Android 14 or up
- enable 3rd party passkeys in chrome. For detail see https://1password.community/discussion/comment/711037/#Comment_711037
- set KeepassDX in the Android setting Passwords & Accounts > Your Provider > Enable
- biometric authentication set up
### working
- sign in with ecdsa/rsa passkeys created by KeepassXC in Chrome. Tested with passkeys.io and webauthn.io.
### maybe working
- sign in with passkeys apps natively (without browser)
- sign in with ecdsa/rsa passkeys created by KeepassXC or KeepassDX in Chrome/Firefox. Tested with
passkeys.io and webauthn.io.
- create new passkeys with ecdsa/rsa in root group (compatible with KeepassXC)
- update existing passkeys
### not working
- create passkeys
- user credential provider with username/password
- open KeepassDX to unlock the database, if it is locked (currently a dummy entry with title unlock db is shown)
- support for username/password see provider.xml
- go back after unlocking a database, if all databases are locked
- support for native apps (implementation is included, but disable in AppRelyingPartyRelation to
prevent phishing)
- select the group, where the new passkeys are saved
- strings in non-english
- respect excludeCredentials
- other userVerification methode other than strong biometric

View File

@@ -44,7 +44,7 @@
android:largeHeap="true"
android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="s"
tools:targetApi="tiramisu"
android:enableOnBackInvokedCallback="true">
<meta-data
@@ -201,13 +201,17 @@
</intent-filter>
</activity>
<activity android:name="com.kunzisoft.keepass.credentialprovider.CredentialProviderActivity"
android:label="CredentialProviderActivity"
android:exported="true">
<intent-filter>
<action android:name="com.kunzisoft.keepass.credentialprovider.GET_PASSKEY"/>
</intent-filter>
</activity>
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.CreatePasskeyActivity"
android:label="CreatePasskeyActivity"
android:exported="true"
tools:targetApi="upside_down_cake" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.UsePasskeyActivity"
android:label="UsePasskeyActivity"
android:exported="true"
tools:targetApi="upside_down_cake" />
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
@@ -259,12 +263,14 @@
</intent-filter>
</service>
<service android:name="com.kunzisoft.keepass.credentialprovider.KeePassDXCredentialProviderService"
<service
android:name="com.kunzisoft.keepass.credentialprovider.service.KeePassDXCredentialProviderService"
android:enabled="true"
android:exported="true"
android:label="KeyPassDX Credential Provider"
android:icon="@mipmap/ic_launcher"
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE">
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
tools:targetApi="upside_down_cake">
<intent-filter>
<action android:name="android.service.credentials.CredentialProviderService" />
</intent-filter>

View File

@@ -5,8 +5,8 @@ import android.os.Bundle
import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir
@@ -41,6 +41,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
return true
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
@@ -48,6 +49,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
super.onDestroy()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)
@@ -77,7 +79,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
mDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
fixDuplicateUuid
)
}
protected fun closeDatabase() {

View File

@@ -1,185 +0,0 @@
package com.kunzisoft.keepass.credentialprovider
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.webauthn.MyAuthenticatorAssertionResponse
import androidx.credentials.webauthn.FidoPublicKeyCredential
import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions
import com.kunzisoft.signature.Signature
import com.kunzisoft.keepass.activities.legacy.DatabaseActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import org.apache.commons.codec.binary.Base64
import androidx.biometric.BiometricPrompt;
import com.kunzisoft.keepass.R
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class CredentialProviderActivity : DatabaseActivity() {
@SuppressLint("RestrictedApi")
private fun validatePasskey(requestJson: String, origin: String, packageName: String, uid: ByteArray, username: Any, credId: ByteArray, privateKey: String) {
val request = PublicKeyCredentialRequestOptions(requestJson)
// https://www.w3.org/TR/webauthn-3/#authdata-flags
val userPresent = true
val userVerified = true
val backupEligibility = true
val backupState = true
val response = MyAuthenticatorAssertionResponse(
requestOptions = request,
credentialId = credId,
origin = origin,
up = userPresent,
uv = userVerified,
be = backupEligibility,
bs = backupState,
userHandle = uid
)
val messageToSign = response.dataToSign()
val sig = Signature.sign(privateKey, messageToSign)
response.signature = sig
val credential = FidoPublicKeyCredential(
rawId = credId, response = response, authenticatorAttachment = "platform"
)
val result = Intent()
val cJson = credential.json()
Log.w("", cJson)
val passkeyCredential = PublicKeyCredential(cJson)
PendingIntentHandler.setGetCredentialResponse(
result, GetCredentialResponse(passkeyCredential)
)
setResult(RESULT_OK, result)
finish()
}
private fun b64Decode(encodedString: String?): ByteArray {
return Base64.decodeBase64(encodedString)
}
private fun cleanUp() {
setResult(RESULT_CANCELED)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
Log.d(javaClass.simpleName,"onDatabaseRetrieved called: database = $database")
super.onDatabaseRetrieved(database)
val getRequest =
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
if (getRequest?.credentialOptions?.size != 1) {
throw Exception("not exact 1 credentialOption")
}
when (val credOption = getRequest.credentialOptions[0]) {
is GetPublicKeyCredentialOption -> handlePublicKeyCredOption(credOption, getRequest.callingAppInfo)
is GetPasswordOption -> handlePasswordOption(credOption)
else -> throw Exception("unknown type of credentialOption")
}
Log.d(javaClass.simpleName, "onDatabaseRetrieved finished")
}
private fun handlePublicKeyCredOption(publicKeyRequest: GetPublicKeyCredentialOption, callingAppInfo: CallingAppInfo) {
val requestInfo = intent.getBundleExtra(KeePassDXCredentialProviderService.INTENT_EXTRA_KEY)
val nodeId = requestInfo?.getString(KeePassDXCredentialProviderService.NODE_ID_KEY)
Log.d(javaClass.simpleName, "nodeId = $nodeId")
if (mDatabase == null || nodeId == null) {
cleanUp()
return
}
val passkey = PasskeyUtil.searchPassKeyByNodeId(mDatabase!!, nodeId)
if (passkey == null) {
cleanUp()
return
}
Log.d(javaClass.simpleName, "passkey found")
val credId = b64Decode(passkey.credId)
val privateKey = passkey.privateKeyPem
val uid = b64Decode(passkey.userHandle)
val origin = appInfoToOrigin(callingAppInfo)
Log.d(javaClass.simpleName, "origin = $origin")
val packageName = callingAppInfo.packageName
val biometricPrompt = BiometricPrompt(
this,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
cleanUp()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
cleanUp()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
validatePasskey(
publicKeyRequest.requestJson,
origin!!,
packageName,
uid,
passkey.username,
credId,
privateKey
)
}
}
)
val title = getString(R.string.passkey_biometric_prompt_title)
val subtitle = getString(R.string.passkey_biometric_prompt_subtitle, origin)
val negativeButtonText = getString(R.string.passkey_biometric_prompt_negative_button_text)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText(negativeButtonText)
.build()
biometricPrompt.authenticate(promptInfo)
}
private fun handlePasswordOption(passwordOption: GetPasswordOption) {
// TODO
}
private fun appInfoToOrigin(callingAppInfo: CallingAppInfo): String? {
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
return callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
}
}

View File

@@ -1,179 +0,0 @@
package com.kunzisoft.keepass.credentialprovider;
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import android.os.CancellationSignal;
import android.os.OutcomeReceiver;
import android.provider.ContactsContract.Directory.PACKAGE_NAME
import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException;
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse;
import androidx.credentials.provider.BeginGetPasswordOption
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.CredentialProviderService
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import androidx.credentials.provider.PublicKeyCredentialEntry
import org.json.JSONObject
import android.util.Log
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
@RequiresApi(value = 34)
class KeePassDXCredentialProviderService : CredentialProviderService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: Database? = null
override fun onCreate() {
super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
override fun onBeginCreateCredentialRequest(request: BeginCreateCredentialRequest, cancellationSignal: CancellationSignal, callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>) {
TODO("Not yet implemented")
}
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
) {
/*
val unlockEntryTitle = "Authenticate to continue"
if (isAppLocked()) {
callback.onResult(BeginGetCredentialResponse(
authenticationActions = mutableListOf(AuthenticationAction(
unlockEntryTitle, createUnlockPendingIntent())
)
)
)
return
}
*/
try {
val response = processGetCredentialsRequest(request)
callback.onResult(response)
} catch (e: GetCredentialException) {
callback.onError(GetCredentialUnknownException())
}
}
companion object {
private const val GET_PASSKEY_INTENT_ACTION = "com.kunzisoft.keepass.credentialprovider.GET_PASSKEY"
private const val GET_PASSWORD_INTENT_ACTION = "com.kunzisoft.keepass.credentialprovider.GET_PASSWORD"
const val NODE_ID_KEY = "nodeId"
const val INTENT_EXTRA_KEY = "CREDENTIAL_DATA"
}
private fun processGetCredentialsRequest(
request: BeginGetCredentialRequest
): BeginGetCredentialResponse {
val callingAppInfo = request.callingAppInfo ?: throw Exception("callingAppInfo is null")
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
for (option in request.beginGetCredentialOptions) {
when (option) {
is BeginGetPasswordOption -> {
// TODO
}
is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll(
populatePasskeyData(callingAppInfo, option)
)
} else -> {
Log.d(javaClass.simpleName,"Request not supported")
}
}
}
return BeginGetCredentialResponse(credentialEntries)
}
private fun populatePasskeyData(callingAppInfo: CallingAppInfo, option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
val json = JSONObject(option.requestJson)
val relyingPartyId = json.optString("rpId", "")
val passkeys = getCredentialsFromDb(relyingPartyId)
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
for (passkey in passkeys) {
val data = Bundle()
data.putString(NODE_ID_KEY, passkey.nodeId)
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = passkey.username,
pendingIntent = createNewPendingIntent(
GET_PASSKEY_INTENT_ACTION,
data
),
beginGetPublicKeyCredentialOption = option,
displayName = passkey.displayName,
lastUsedTime = passkey.lastUsedTime,
isAutoSelectAllowed = false
)
)
}
return passkeyEntries
}
private fun getCredentialsFromDb(relyingPartyId: String) : List<PasskeyUtil.Passkey> {
if (mDatabase == null) {
// TODO make sure that the database is open
val dummyPassKey = PasskeyUtil.Passkey("", "unknown", "unlock db", "", "", "", "", null)
return listOf(dummyPassKey)
}
val passkeys = PasskeyUtil.searchPasskeys(mDatabase!!)
val passkeysMatching = passkeys.filter { p -> p.relyingParty == relyingPartyId }
return passkeysMatching
}
private fun createNewPendingIntent(action: String, extra: Bundle? = null): PendingIntent {
val intent = Intent(action).setPackage(PACKAGE_NAME).setClass(applicationContext, CredentialProviderActivity::class.java)
if (extra != null) {
intent.putExtra(INTENT_EXTRA_KEY, extra)
}
val requestCode = 42 // not used
return PendingIntent.getActivity(
applicationContext, requestCode, intent,
(PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
)
}
override fun onClearCredentialStateRequest(request: ProviderClearCredentialStateRequest, cancellationSignal: CancellationSignal, callback: OutcomeReceiver<Void?, ClearCredentialException>) {
// nothing to do
}
}

View File

@@ -1,114 +0,0 @@
/*
* 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 androidx.credentials.webauthn
import android.annotation.SuppressLint
import android.util.Log
import java.security.MessageDigest
import org.json.JSONObject
import org.apache.commons.codec.binary.Base64
@SuppressLint("RestrictedApi")
class MyAuthenticatorAssertionResponse(
private val requestOptions: PublicKeyCredentialRequestOptions,
private val credentialId: ByteArray,
private val origin: String,
private val up: Boolean,
private val uv: Boolean,
private val be: Boolean,
private val bs: Boolean,
private var userHandle: ByteArray,
private val packageName: String? = null,
private val clientDataHash: ByteArray? = null,
) : AuthenticatorResponse {
override var clientJson = JSONObject()
var authenticatorData: ByteArray
var signature: ByteArray = byteArrayOf()
init {
clientJson.put("type", "webauthn.get")
clientJson.put("challenge", b64Encode(requestOptions.challenge))
clientJson.put("origin", origin)
clientJson.put("crossOrigin", false)
if (packageName != null) {
clientJson.put("androidPackageName", packageName)
}
authenticatorData = defaultAuthenticatorData()
}
fun defaultAuthenticatorData(): ByteArray {
val md = MessageDigest.getInstance("SHA-256")
val rpHash = md.digest(requestOptions.rpId.toByteArray())
var flags: Int = 0
if (up) {
flags = flags or 0x01
}
if (uv) {
flags = flags or 0x04
}
if (be) {
flags = flags or 0x08
}
if (bs) {
flags = flags or 0x10
}
val ret = rpHash + byteArrayOf(flags.toByte()) + byteArrayOf(0, 0, 0, 0)
return ret
}
fun dataToSign(): ByteArray {
val md = MessageDigest.getInstance("SHA-256")
var hash: ByteArray
if (clientDataHash != null) {
hash = clientDataHash
} else {
hash = md.digest(temp().toByteArray())
}
return authenticatorData + hash
}
override fun json(): JSONObject {
val clientJsonTemp = temp()
Log.w("", clientJsonTemp)
val clientData = clientJsonTemp.toByteArray()
val response = JSONObject()
if (clientDataHash == null) {
response.put("clientDataJSON", b64Encode(clientData))
}
response.put("authenticatorData", b64Encode(authenticatorData))
response.put("signature", b64Encode(signature))
response.put("userHandle", b64Encode(userHandle))
return response
}
fun temp(): String {
val clientJsonTemp = clientJson.toString()
val clientJsonGood = clientJsonTemp.replace("\\/", "/")
return clientJsonGood
}
private fun b64Decode(encodedString: String?): ByteArray {
return Base64.decodeBase64(encodedString)
}
private fun b64Encode(binData: ByteArray?): String {
return Base64.encodeBase64URLSafeString(binData)
}
}

View File

@@ -1,93 +0,0 @@
package com.kunzisoft.keepass.credentialprovider
import android.os.Build
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.utils.UuidUtil
import java.time.Instant
class PasskeyUtil {
data class Passkey(val nodeId: String, val username: String, val displayName: String, val privateKeyPem: String, val credId: String, val userHandle: String, val relyingParty: String, val lastUsedTime: Instant?)
companion object {
const val PASSKEY_TAG = "Passkey"
fun convertEntryToPasskey(entry: Entry): Passkey? {
if (!entry.tags.toList().contains(PASSKEY_TAG)) {
return null
}
val nodeId = UuidUtil.toHexString(entry.nodeId.id)!!
val displayName = entry.getVisualTitle()
val lastUsedTime = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
entry.lastAccessTime.date.toInstant()
} else {
null
}
var username = ""
var privateKeyPem = ""
var credId = ""
var userHandle = ""
var relyingParty = ""
for (field in entry.getExtraFields()) {
val fieldName = field.name
// field names from KeypassXC are used
if (fieldName == "KPEX_PASSKEY_USERNAME") {
username = field.protectedValue.stringValue
} else if (field.name == "KPEX_PASSKEY_PRIVATE_KEY_PEM") {
privateKeyPem = field.protectedValue.stringValue
} else if (field.name == "KPEX_PASSKEY_CREDENTIAL_ID") {
credId = field.protectedValue.stringValue
} else if (field.name == "KPEX_PASSKEY_USER_HANDLE") {
userHandle = field.protectedValue.stringValue
} else if (field.name == "KPEX_PASSKEY_RELYING_PARTY") {
relyingParty = field.protectedValue.stringValue
}
// KPEX_PASSKEY_RELYING_PARTY
}
return Passkey(nodeId, username, displayName, privateKeyPem, credId, userHandle, relyingParty, lastUsedTime)
}
fun convertEntriesListToPasskeys(entries: List<Entry>): List<Passkey> {
return entries.mapNotNull { e -> convertEntryToPasskey(e) }
}
fun searchPasskeys(database: Database): List<Passkey> {
val searchHelper = SearchHelper()
val searchParameters = SearchParameters().apply {
searchQuery = PASSKEY_TAG
searchInTitles = false
searchInUsernames = false
searchInPasswords = false
searchInUrls = false
searchInNotes = false
searchInOTP = false
searchInOther = false
searchInUUIDs = false
searchInTags = true
searchInCurrentGroup = false
searchInSearchableGroup = false
searchInRecycleBin = false
searchInTemplates = false
}
val searchResult = searchHelper.createVirtualGroupWithSearchResult(database, searchParameters, null, Int.MAX_VALUE)
?: return emptyList()
return convertEntriesListToPasskeys(searchResult.getChildEntries())
}
fun searchPassKeyByNodeId(database: Database, nodeId: String): Passkey? {
val uuidToSearch = UuidUtil.fromHexString(nodeId)!!
val nodeIdUUIDToSearch = NodeIdUUID(uuidToSearch)
val entry = database.getEntryById(nodeIdUUIDToSearch)!!
return convertEntryToPasskey(entry)
}
}
}

View File

@@ -0,0 +1,281 @@
package com.kunzisoft.keepass.credentialprovider.activity
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseActivity
import com.kunzisoft.keepass.credentialprovider.data.Passkey
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.util.AppRelyingPartyRelation
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper
import com.kunzisoft.keepass.credentialprovider.util.DatabaseHelper
import com.kunzisoft.keepass.credentialprovider.util.IntentHelper
import com.kunzisoft.keepass.credentialprovider.util.JsonHelper
import com.kunzisoft.keepass.credentialprovider.util.OriginHelper
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.random.KeePassDXRandom
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class CreatePasskeyActivity : DatabaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(javaClass.simpleName, "onCreate called")
super.onCreate(savedInstanceState)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
Log.d(javaClass.simpleName, "onDatabaseRetrieved called")
super.onDatabaseRetrieved(database)
try {
if (database == null) {
throw CreateCredentialUnknownException("retrievedDatabase is null, maybe database is locked")
}
if (intent == null) {
throw CreateCredentialUnknownException("intent is null")
}
if (mDatabaseTaskProvider == null) {
throw CreateCredentialUnknownException("mDatabaseTaskProvider is null")
}
createPasskeyAfterPrompt(database, mDatabaseTaskProvider!!, intent)
} catch (e: CreateCredentialUnknownException) {
Log.e(this::class.java.simpleName, "CreateCredentialUnknownException was thrown", e)
setResult(RESULT_CANCELED)
finish()
} catch (e: Exception) {
Log.e(this::class.java.simpleName, "other exception was thrown", e)
setResult(RESULT_CANCELED)
finish()
}
}
private fun createPasskeyAfterPrompt(
database: ContextualDatabase,
databaseTaskProvider: DatabaseTaskProvider,
intent: Intent
) {
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
}
val publicKeyRequest = request.callingRequest as CreatePublicKeyCredentialRequest
val creationOptions = JsonHelper.parseJsonToCreateOptions(publicKeyRequest.requestJson)
val relyingParty = creationOptions.relyingParty
val biometricPrompt = BiometricPrompt(
this,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int, errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString)
throw CreateCredentialUnknownException("authentication error: errorCode = $errorCode, errString = $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
throw CreateCredentialUnknownException("authentication failed")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
createPasskey(database, databaseTaskProvider, intent, creationOptions)
}
}
)
val title = getString(R.string.passkey_creation_biometric_prompt_title)
val subtitle =
getString(
R.string.passkey_creation_biometric_prompt_subtitle,
relyingParty
)
val negativeButtonText =
getString(R.string.passkey_creation_biometric_prompt_negative_button_text)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText(negativeButtonText)
.build()
biometricPrompt.authenticate(promptInfo)
}
private fun createPasskey(
database: ContextualDatabase,
databaseTaskProvider: DatabaseTaskProvider,
intent: Intent,
creationOptions: PublicKeyCredentialCreationOptions
) {
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val nodeId = IntentHelper.getVerifiedNodeId(intent)
?: throw CreateCredentialUnknownException("could not get verified nodeId from intent")
val callingAppInfo = request!!.callingAppInfo
val relyingParty = creationOptions.relyingParty
val challenge = creationOptions.challenge
val keyTypeIdList = creationOptions.keyTypeIdList
val webOrigin = OriginHelper.getWebOrigin(callingAppInfo, assets)
val apkSigningCertificate =
callingAppInfo.signingInfo.apkContentsSigners.getOrNull(0)?.toByteArray()
createPasskeyWithParameters(
relyingParty,
creationOptions.username,
creationOptions.userId,
database,
databaseTaskProvider,
keyTypeIdList,
challenge,
webOrigin,
apkSigningCertificate,
nodeId
)
}
private fun createPasskeyWithParameters(
relyingParty: String,
username: String,
userHandle: ByteArray,
database: ContextualDatabase,
databaseTaskProvider: DatabaseTaskProvider,
keyTypeIdList: List<Long>,
challenge: ByteArray,
webOrigin: String?,
apkSigningCertificate: ByteArray?,
nodeId: String
) {
val isPrivilegedApp =
(webOrigin != null && webOrigin == OriginHelper.DEFAULT_PROTOCOL + relyingParty)
Log.d(this::class.java.simpleName, "isPrivilegedApp = $isPrivilegedApp")
if (!isPrivilegedApp) {
val isValid =
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
if (!isValid) {
throw CreateCredentialUnknownException(
"could not verify relation between app " +
"and relyingParty $relyingParty"
)
}
}
val credentialId = KeePassDXRandom.generateCredentialId()
val (keyPair, keyTypeId) = Signature.generateKeyPair(keyTypeIdList)
?: throw CreateCredentialUnknownException("no known public key type found")
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
if (IntentHelper.isPlaceholderNodeId(nodeId)) {
// create new entry in database
val displayName = "$relyingParty (Passkey)"
val newPasskey = Passkey(
nodeId = "", // created by the database
username = username,
displayName = displayName,
privateKeyPem = privateKeyPem,
credId = Base64Helper.b64Encode(credentialId),
userHandle = Base64Helper.b64Encode(userHandle),
relyingParty = relyingParty,
databaseEntry = null
)
DatabaseHelper.saveNewEntry(database, databaseTaskProvider, newPasskey)
} else {
// update an existing entry in database
val oldPasskey = DatabaseHelper.searchPassKeyByNodeId(database, nodeId)
?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found")
val updatedPasskey = Passkey(
nodeId = "", // unchanged
username = username,
displayName = oldPasskey.displayName,
privateKeyPem = privateKeyPem,
credId = Base64Helper.b64Encode(credentialId),
userHandle = Base64Helper.b64Encode(userHandle),
relyingParty = relyingParty,
databaseEntry = oldPasskey.databaseEntry
)
DatabaseHelper.updateEntry(database, databaseTaskProvider, updatedPasskey)
}
val publicKeyEncoded = Signature.convertPublicKey(keyPair.public, keyTypeId)
val publicKeyMap = Signature.convertPublicKeyToMap(keyPair.public, keyTypeId)
val publicKeyCbor = JsonHelper.generateCborFromMap(publicKeyMap!!)
val authData = JsonHelper.generateAuthDataForCreate(
userPresent = true,
userVerified = true,
backupEligibility = true,
backupState = true,
rpId = relyingParty.toByteArray(),
credentialId = credentialId,
credentialPublicKey = publicKeyCbor
)
val attestationObject = JsonHelper.generateAttestationObject(authData)
val clientJson: String
if (isPrivilegedApp) {
clientJson = JsonHelper.generateClientDataJsonPrivileged()
} else {
val origin = OriginHelper.DEFAULT_PROTOCOL + relyingParty
clientJson = JsonHelper.generateClientDataJsonNonPrivileged(
challenge,
origin,
packageName,
isCrossOriginAdded = true,
isGet = false
)
}
val responseJson = JsonHelper.createAuthenticatorAttestationResponseJSON(
credentialId,
clientJson,
attestationObject,
publicKeyEncoded!!,
authData,
keyTypeId
)
// log only the length to prevent logging sensitive information
Log.d(javaClass.simpleName, "responseJson with length ${responseJson.length} created")
val createPublicKeyCredResponse = CreatePublicKeyCredentialResponse(responseJson)
val resultOfActivity = Intent()
PendingIntentHandler.setCreateCredentialResponse(
resultOfActivity, createPublicKeyCredResponse
)
setResult(Activity.RESULT_OK, resultOfActivity)
finish()
}
}

View File

@@ -0,0 +1,236 @@
package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseActivity
import com.kunzisoft.keepass.credentialprovider.data.Passkey
import com.kunzisoft.keepass.credentialprovider.util.AppRelyingPartyRelation
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper
import com.kunzisoft.keepass.credentialprovider.util.DatabaseHelper
import com.kunzisoft.keepass.credentialprovider.util.IntentHelper
import com.kunzisoft.keepass.credentialprovider.util.JsonHelper
import com.kunzisoft.keepass.credentialprovider.util.OriginHelper
import com.kunzisoft.keepass.credentialprovider.util.OriginHelper.Companion.DEFAULT_PROTOCOL
import com.kunzisoft.keepass.database.ContextualDatabase
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class UsePasskeyActivity : DatabaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(javaClass.simpleName, "onCreate called")
super.onCreate(savedInstanceState)
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
Log.d(javaClass.simpleName, "onDatabaseRetrieved called")
super.onDatabaseRetrieved(database)
try {
if (database == null) {
throw CreateCredentialUnknownException("retrievedDatabase is null, maybe database is locked")
}
if (intent == null) {
throw CreateCredentialUnknownException("intent is null")
}
usePasskeyAfterPrompt(database, intent)
} catch (e: CreateCredentialUnknownException) {
Log.e(this::class.java.simpleName, "CreateCredentialUnknownException was thrown", e)
setResult(RESULT_CANCELED)
finish()
} catch (e: Exception) {
Log.e(this::class.java.simpleName, "other exception was thrown", e)
setResult(RESULT_CANCELED)
finish()
}
}
private fun usePasskeyAfterPrompt(
database: ContextualDatabase,
intent: Intent
) {
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
if (request.credentialOptions.size != 1) {
throw GetCredentialUnknownException("not exact one credentialOption")
}
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
}
val credentialOption = request.credentialOptions[0] as GetPublicKeyCredentialOption
val clientDataHash = credentialOption.clientDataHash
val requestOptions = JsonHelper.parseJsonToRequestOptions(credentialOption.requestJson)
val relyingParty = requestOptions.relyingParty
val challenge = Base64Helper.b64Decode(requestOptions.challengeString)
val packageName = request.callingAppInfo.packageName
val webOrigin = OriginHelper.getWebOrigin(request.callingAppInfo, assets)
val isPrivilegedApp =
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty && clientDataHash != null)
Log.d(javaClass.simpleName, "isPrivilegedApp = $isPrivilegedApp")
if (!isPrivilegedApp) {
val apkSigners = request.callingAppInfo.signingInfo.apkContentsSigners
val apkSigningCertificate = apkSigners.getOrNull(0)?.toByteArray()
val isValid =
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
if (!isValid) {
throw CreateCredentialUnknownException(
"could not verify relation between app " +
"and relyingParty $relyingParty"
)
}
}
val nodeId = IntentHelper.getVerifiedNodeId(intent)
?: throw GetCredentialUnknownException("could not get verified nodeId from intent")
val passkey = DatabaseHelper.searchPassKeyByNodeId(database, nodeId)
?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found")
usePasskeyAfterPromptWithParameters(
relyingParty,
packageName,
clientDataHash,
isPrivilegedApp,
challenge,
passkey
)
}
private fun usePasskeyAfterPromptWithParameters(
relyingParty: String,
packageName: String,
clientDataHash: ByteArray?,
isPrivilegedApp: Boolean,
challenge: ByteArray,
passkey: Passkey
) {
val biometricPrompt = BiometricPrompt(
this,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
throw GetCredentialUnknownException("authentication error: errorCode = $errorCode, errString = $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
throw GetCredentialUnknownException("authentication failed")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
createResponse(
relyingParty,
packageName,
clientDataHash,
isPrivilegedApp,
challenge,
passkey
)
}
}
)
val title = getString(R.string.passkey_usage_biometric_prompt_title)
val subtitle = getString(R.string.passkey_usage_biometric_prompt_subtitle, relyingParty)
val negativeButtonText =
getString(R.string.passkey_usage_biometric_prompt_negative_button_text)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText(negativeButtonText)
.build()
biometricPrompt.authenticate(promptInfo)
}
private fun createResponse(
relyingParty: String,
packageName: String,
clientDataHash: ByteArray?,
isPrivilegedApp: Boolean,
challenge: ByteArray,
passkey: Passkey
) {
// https://www.w3.org/TR/webauthn-3/#authdata-flags
val userPresent = true
val userVerified = true
val backupEligibility = true
val backupState = true
val authenticatorData = JsonHelper.generateAuthDataForUsage(
relyingParty.toByteArray(),
userPresent,
userVerified,
backupEligibility,
backupState
)
val clientDataJson: String
val dataToSign: ByteArray
if (isPrivilegedApp) {
clientDataJson = JsonHelper.generateClientDataJsonPrivileged()
dataToSign =
JsonHelper.generateDataToSignPrivileged(clientDataHash!!, authenticatorData)
} else {
val origin = DEFAULT_PROTOCOL + relyingParty
clientDataJson = JsonHelper.generateClientDataJsonNonPrivileged(
challenge,
origin,
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.credId
)
val result = Intent()
val passkeyCredential = PublicKeyCredential(getCredentialResponse)
PendingIntentHandler.setGetCredentialResponse(
result, GetCredentialResponse(passkeyCredential)
)
setResult(RESULT_OK, result)
finish()
}
}

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.credentialprovider.data
import com.kunzisoft.keepass.database.element.Entry
data class Passkey(
val nodeId: String,
val username: String,
val displayName: String,
val privateKeyPem: String,
val credId: String,
val userHandle: String,
val relyingParty: String,
val databaseEntry: Entry?
)

View File

@@ -0,0 +1,9 @@
package com.kunzisoft.keepass.credentialprovider.data
data class PublicKeyCredentialCreationOptions(
val relyingParty: String,
val challenge: ByteArray,
val username: String,
val userId: ByteArray,
val keyTypeIdList: List<Long>
)

View File

@@ -0,0 +1,7 @@
package com.kunzisoft.keepass.credentialprovider.data
data class PublicKeyCredentialRequestOptions(
val relyingParty: String,
val challengeString: String
) {
}

View File

@@ -0,0 +1,213 @@
package com.kunzisoft.keepass.credentialprovider.service
import android.os.Build
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.CredentialProviderService
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.data.Passkey
import com.kunzisoft.keepass.credentialprovider.util.DatabaseHelper
import com.kunzisoft.keepass.credentialprovider.util.IntentHelper
import com.kunzisoft.keepass.credentialprovider.util.JsonHelper
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import java.time.Instant
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class KeePassDXCredentialProviderService : CredentialProviderService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: Database? = null
override fun onCreate() {
super.onCreate()
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.registerProgressTask()
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
this.mDatabase = database
}
}
override fun onDestroy() {
mDatabaseTaskProvider?.unregisterProgressTask()
super.onDestroy()
}
override fun onBeginCreateCredentialRequest(
request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
if (response != null) {
callback.onResult(response)
} else {
callback.onError(CreateCredentialUnknownException())
}
}
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type
return handleCreatePasskeyQuery(request)
}
}
// request type not supported
Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
return null
}
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
if (mDatabase == null) {
// database is locked, a dummy entry is shown.
val messageToUnlockDatabase = getString(R.string.passkey_usage_unlock_database_message)
val dummyEntryList = listOf(
CreateEntry(
getString(R.string.passkey_unknown_username),
IntentHelper.generateUnlockPendingIntent(applicationContext),
messageToUnlockDatabase
)
)
return BeginCreateCredentialResponse(dummyEntryList)
}
val createEntries: MutableList<CreateEntry> = mutableListOf()
val accountName = mDatabase!!.name
val descriptionNewEntry = getString(R.string.passkey_creation_description)
val createPendingIntentNewEntry =
IntentHelper.generateCreatePendingIntent(applicationContext)!!
createEntries.add(
CreateEntry(
accountName,
createPendingIntentNewEntry,
descriptionNewEntry
)
)
val relyingParty = JsonHelper.parseJsonToCreateOptions(request.requestJson).relyingParty
val passkeyList = getCredentialsFromDb(relyingParty, mDatabase!!)
for (passkey in passkeyList) {
val createPendingIntent =
IntentHelper.generateCreatePendingIntent(applicationContext, passkey.nodeId)!!
val description = getString(R.string.passkey_update_description, passkey.displayName)
createEntries.add(
CreateEntry(
accountName,
createPendingIntent,
description
)
)
}
return BeginCreateCredentialResponse(createEntries)
}
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
) {
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
val response = processGetCredentialsRequest(request)
if (response != null) {
callback.onResult(response)
} else {
callback.onError(GetCredentialUnknownException())
}
}
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
for (option in request.beginGetCredentialOptions) {
when (option) {
is BeginGetPublicKeyCredentialOption -> {
credentialEntries.addAll(
populatePasskeyData(option)
)
return BeginGetCredentialResponse(credentialEntries)
}
}
}
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
return null
}
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
val relyingParty = JsonHelper.parseJsonToRequestOptions(option.requestJson).relyingParty
if (relyingParty.isBlank()) {
throw CreateCredentialUnknownException("relying party id is null or blank")
}
if (mDatabase == null) {
val unknownUsername = getString(R.string.passkey_unknown_username)
val messageToUnlockDatabase = getString(R.string.passkey_usage_unlock_database_message)
val unlockPendingIntent = IntentHelper.generateUnlockPendingIntent(applicationContext)
val entry = PublicKeyCredentialEntry(
context = applicationContext,
username = unknownUsername,
pendingIntent = unlockPendingIntent,
beginGetPublicKeyCredentialOption = option,
displayName = messageToUnlockDatabase,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = true
)
return listOf(entry)
}
val passkeys = getCredentialsFromDb(relyingParty, mDatabase!!)
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
for (passkey in passkeys) {
val usagePendingIntent =
IntentHelper.generateUsagePendingIntent(applicationContext, passkey.nodeId)!!
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = passkey.username,
pendingIntent = usagePendingIntent,
beginGetPublicKeyCredentialOption = option,
displayName = passkey.displayName,
isAutoSelectAllowed = false
)
)
}
return passkeyEntries
}
private fun getCredentialsFromDb(relyingPartyId: String, database: Database): List<Passkey> {
val passkeys = DatabaseHelper.getAllPasskeys(database)
val passkeysMatching = passkeys.filter { p -> p.relyingParty == relyingPartyId }
return passkeysMatching
}
override fun onClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>
) {
// nothing to do
}
}

View File

@@ -0,0 +1,18 @@
package com.kunzisoft.keepass.credentialprovider.util
class AppRelyingPartyRelation {
companion object {
fun isRelationValid(relyingParty: String, apkSigningCertificate: ByteArray?): Boolean {
/*
TODO
to implement this, a request to https://$rp/.well-known/assetlinks.json,
parsing the result and matching the hash of the apkSigningCertificate is needed.
This is needed to make sure that a malicious app can not act as an arbitrary relying party.
In short: prevent phishing
*/
return false
}
}
}

View File

@@ -0,0 +1,20 @@
package com.kunzisoft.keepass.credentialprovider.util
import org.apache.commons.codec.binary.Base64
class Base64Helper {
companion object {
fun b64Decode(encodedString: String?): ByteArray {
return Base64.decodeBase64(encodedString)
}
fun b64Encode(data: ByteArray): String {
return android.util.Base64.encodeToString(
data,
android.util.Base64.NO_PADDING or android.util.Base64.NO_WRAP or android.util.Base64.URL_SAFE
)
}
}
}

View File

@@ -0,0 +1,98 @@
package com.kunzisoft.keepass.credentialprovider.util
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.credentialprovider.data.Passkey
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.utils.UuidUtil
@RequiresApi(Build.VERSION_CODES.O)
class DatabaseHelper {
companion object {
fun getAllPasskeys(database: Database): List<Passkey> {
val searchHelper = SearchHelper()
val searchParameters = SearchParameters().apply {
searchQuery = PasskeyConverter.PASSKEY_TAG
searchInTitles = false
searchInUsernames = false
searchInPasswords = false
searchInUrls = false
searchInNotes = false
searchInOTP = false
searchInOther = false
searchInUUIDs = false
searchInTags = true
searchInCurrentGroup = false
searchInSearchableGroup = false
searchInRecycleBin = false
searchInTemplates = false
}
val fromGroup = null
val max = Int.MAX_VALUE
val searchResult = searchHelper.createVirtualGroupWithSearchResult(
database,
searchParameters,
fromGroup,
max
)
?: return emptyList()
return PasskeyConverter.convertEntriesListToPasskeys(searchResult.getChildEntries())
}
fun searchPassKeyByNodeId(database: Database, nodeId: String): Passkey? {
val uuidToSearch = UuidUtil.fromHexString(nodeId) ?: return null
val nodeIdUUIDToSearch = NodeIdUUID(uuidToSearch)
val entry = database.getEntryById(nodeIdUUIDToSearch) ?: return null
return PasskeyConverter.convertEntryToPasskey(entry)
}
fun updateEntry(
database: Database,
databaseTaskProvider: DatabaseTaskProvider,
updatedPasskey: Passkey
) {
val oldEntry = Entry(updatedPasskey.databaseEntry!!)
val entryToUpdate = Entry(updatedPasskey.databaseEntry)
PasskeyConverter.setPasskeyInEntry(updatedPasskey, entryToUpdate)
entryToUpdate.setEntryInfo(
database,
entryToUpdate.getEntryInfo(
database,
raw = true,
removeTemplateConfiguration = false
)
)
val save = true
databaseTaskProvider.startDatabaseUpdateEntry(oldEntry, entryToUpdate, save)
Log.d(this::class.java.simpleName, "passkey in entry ${oldEntry.title} updated")
}
fun saveNewEntry(
database: ContextualDatabase,
databaseTaskProvider: DatabaseTaskProvider,
newPasskey: Passkey
) {
val newEntry = database.createEntry() ?: throw Exception("can not create new entry")
PasskeyConverter.setPasskeyInEntry(newPasskey, newEntry)
val save = true
val group = database.rootGroup
?: throw Exception("can not save new entry in database, because rootGroup is null")
databaseTaskProvider.startDatabaseCreateEntry(newEntry, group, save)
Log.d(this::class.java.simpleName, "new entry saved")
}
}
}

View File

@@ -0,0 +1,195 @@
package com.kunzisoft.keepass.credentialprovider.util
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.credentialprovider.activity.CreatePasskeyActivity
import com.kunzisoft.keepass.credentialprovider.activity.UsePasskeyActivity
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import java.security.KeyStore
import java.security.MessageDigest
import java.time.Instant
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey
@RequiresApi(Build.VERSION_CODES.O)
class IntentHelper {
companion object {
private const val HMAC_TYPE = "HmacSHA256"
private const val KEY_NODE_ID = "nodeId"
private const val KEY_TIMESTAMP = "timestamp"
private const val KEY_AUTHENTICATION_CODE = "authenticationCode"
private const val SEPARATOR = "_"
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
private const val KEYSTORE_TYPE = "AndroidKeyStore"
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
private val REGEX_NODE_ID = "[A-F0-9]{32}".toRegex()
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
private const val MAX_DIFF_IN_SECONDS = 60
private var currentRequestCode: Int = 0
private fun <T : Activity> createPendingIntent(
clazz: Class<T>,
applicationContext: Context,
data: Bundle? = null
): PendingIntent {
val intent = Intent().setClass(applicationContext, clazz)
data?.let { intent.putExtras(data) }
val requestCode = currentRequestCode
// keeps the requestCodes unique, the limit is arbitrary
currentRequestCode = (currentRequestCode + 1) % 1000
return PendingIntent.getActivity(
applicationContext,
requestCode,
intent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
private fun <T : Activity> createPendingIntentWithAuthenticationCode(
clazz: Class<T>,
applicationContext: Context,
nodeId: String
): PendingIntent? {
if (nodeId.matches(REGEX_NODE_ID).not()) return null
val data = Bundle()
val timestamp = Instant.now().epochSecond.toString()
data.putString(KEY_NODE_ID, nodeId)
data.putString(KEY_TIMESTAMP, timestamp)
val message = nodeId + SEPARATOR + timestamp
val authenticationCode = generateAuthenticationCode(message).toHexString()
data.putString(KEY_AUTHENTICATION_CODE, authenticationCode)
return createPendingIntent(clazz, applicationContext, data)
}
fun generateUnlockPendingIntent(applicationContext: Context): PendingIntent {
// TODO after the database is unlocked by the user, return to the flow
return createPendingIntent(FileDatabaseSelectActivity::class.java, applicationContext)
}
fun generateCreatePendingIntent(
applicationContext: Context,
nodeId: String = PLACEHOLDER_FOR_NEW_NODE_ID
): PendingIntent? {
return createPendingIntentWithAuthenticationCode(
CreatePasskeyActivity::class.java,
applicationContext,
nodeId
)
}
fun generateUsagePendingIntent(
applicationContext: Context,
nodeId: String
): PendingIntent? {
return createPendingIntentWithAuthenticationCode(
UsePasskeyActivity::class.java,
applicationContext,
nodeId
)
}
fun getVerifiedNodeId(intent: Intent): String? {
val nodeId = intent.getStringExtra(KEY_NODE_ID) ?: return null
val timestampString = intent.getStringExtra(KEY_TIMESTAMP) ?: return null
val authenticationCode = intent.getStringExtra(KEY_AUTHENTICATION_CODE) ?: return null
if (nodeId.matches(REGEX_NODE_ID).not() ||
timestampString.matches(REGEX_TIMESTAMP).not() ||
authenticationCode.matches(REGEX_AUTHENTICATION_CODE).not()
) {
return null
}
val diff = Instant.now().epochSecond - timestampString.toLong()
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
return null
}
val message = (nodeId + SEPARATOR + timestampString)
if (verifyAuthenticationCode(
message,
authenticationCode.decodeHexToByteArray()
).not()
) {
return null
}
Log.d(this::class.java.simpleName, "nodeId $nodeId verified")
return nodeId
}
private fun verifyAuthenticationCode(
message: String,
authenticationCodeIn: ByteArray
): Boolean {
val authenticationCode = generateAuthenticationCode(message)
return MessageDigest.isEqual(authenticationCodeIn, authenticationCode)
}
private fun generateAuthenticationCode(message: String): ByteArray {
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
keyStore.load(null)
val hmacKey = try {
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
} catch (e: Exception) {
// key not found
generateKey()
}
val mac = Mac.getInstance(HMAC_TYPE)
mac.init(hmacKey)
val authenticationCode = mac.doFinal(message.toByteArray())
return authenticationCode
}
private fun generateKey(): SecretKey? {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
)
val keySizeInBits = 128
keyGenerator.init(
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
.setKeySize(keySizeInBits)
.build()
)
val key = keyGenerator.generateKey()
return key
}
fun isPlaceholderNodeId(nodeId: String): Boolean {
return nodeId == PLACEHOLDER_FOR_NEW_NODE_ID
}
private fun String.decodeHexToByteArray(): ByteArray {
if (length % 2 != 0) {
throw IllegalArgumentException("Must have an even length")
}
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}
}

View File

@@ -0,0 +1,251 @@
package com.kunzisoft.keepass.credentialprovider.util
import android.annotation.SuppressLint
import androidx.credentials.webauthn.Cbor
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper.Companion.b64Decode
import com.kunzisoft.keepass.credentialprovider.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 "<placeholder>"
}
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<String, Any>()
ao["fmt"] = "none"
ao["attStmt"] = emptyMap<Any, Any>()
ao["authData"] = authData
return generateCborFromMap(ao)
}
@SuppressLint("RestrictedApi")
fun <T> generateCborFromMap(map: Map<T, Any>): 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<Long> = 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()
)
}
}
}

View File

@@ -0,0 +1,22 @@
package com.kunzisoft.keepass.credentialprovider.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
val origin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
return origin
}
}
}

View File

@@ -0,0 +1,120 @@
package com.kunzisoft.keepass.credentialprovider.util
import android.os.Build
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.credentialprovider.data.Passkey
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.UuidUtil
@RequiresApi(Build.VERSION_CODES.O)
class PasskeyConverter {
companion object {
// field names from KeypassXC are used
private const val FIELD_USERNAME = "KPEX_PASSKEY_USERNAME"
private const val FIELD_PRIVATE_KEY = "KPEX_PASSKEY_PRIVATE_KEY_PEM"
private const val FIELD_CREDENTIAL_ID = "KPEX_PASSKEY_CREDENTIAL_ID"
private const val FIELD_USER_HANDLE = "KPEX_PASSKEY_USER_HANDLE"
private const val FIELD_RELYING_PARTY = "KPEX_PASSKEY_RELYING_PARTY"
const val PASSKEY_TAG = "Passkey"
fun convertEntryToPasskey(entry: Entry): Passkey? {
if (entry.tags.toList().contains(PASSKEY_TAG).not()) {
return null
}
val nodeId = UuidUtil.toHexString(entry.nodeId.id) ?: return null
val displayName = entry.getVisualTitle()
var username = ""
var privateKeyPem = ""
var credId = ""
var userHandle = ""
var relyingParty = ""
for (field in entry.getExtraFields()) {
val fieldName = field.name
if (fieldName == FIELD_USERNAME) {
username = field.protectedValue.stringValue
} else if (field.name == FIELD_PRIVATE_KEY) {
privateKeyPem = field.protectedValue.stringValue
} else if (field.name == FIELD_CREDENTIAL_ID) {
credId = field.protectedValue.stringValue
} else if (field.name == FIELD_USER_HANDLE) {
userHandle = field.protectedValue.stringValue
} else if (field.name == FIELD_RELYING_PARTY) {
relyingParty = field.protectedValue.stringValue
}
}
return Passkey(
nodeId,
username,
displayName,
privateKeyPem,
credId,
userHandle,
relyingParty,
entry
)
}
fun convertEntriesListToPasskeys(entries: List<Entry>): List<Passkey> {
return entries.mapNotNull { e -> convertEntryToPasskey(e) }
}
fun setPasskeyInEntry(passkey: Passkey, entry: Entry) {
entry.tags.put(PASSKEY_TAG)
entry.title = passkey.displayName
entry.lastModificationTime = DateInstant()
entry.username = passkey.username
entry.url = OriginHelper.DEFAULT_PROTOCOL + passkey.relyingParty
val protected = true
val unProtected = false
entry.putExtraField(
Field(
FIELD_USERNAME,
ProtectedString(unProtected, passkey.username)
)
)
entry.putExtraField(
Field(
FIELD_PRIVATE_KEY,
ProtectedString(protected, passkey.privateKeyPem)
)
)
entry.putExtraField(
Field(
FIELD_CREDENTIAL_ID,
ProtectedString(protected, passkey.credId)
)
)
entry.putExtraField(
Field(
FIELD_USER_HANDLE,
ProtectedString(protected, passkey.userHandle)
)
)
entry.putExtraField(
Field(
FIELD_RELYING_PARTY,
ProtectedString(unProtected, passkey.relyingParty)
)
)
}
}
}

View File

@@ -108,14 +108,19 @@ class DatabaseTaskProvider(
) {
// To show dialog only if context is an activity
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
catch (_: Exception) { null }
private var activity: FragmentActivity? = try {
context as? FragmentActivity?
} catch (_: Exception) {
null
}
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
var onActionFinish: ((database: ContextualDatabase,
var onActionFinish: ((
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
result: ActionRunnable.Result
) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
@@ -175,7 +180,8 @@ class DatabaseTaskProvider(
}
}
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
private val mActionDatabaseListener =
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
}
@@ -265,7 +271,8 @@ class DatabaseTaskProvider(
}
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
mBinder =
(serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addServiceListeners(this)
getService().checkDatabase()
getService().checkDatabaseInfo()
@@ -296,7 +303,11 @@ class DatabaseTaskProvider(
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
context.bindService(
intentDatabaseTask,
it,
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
)
}
}
@@ -324,6 +335,7 @@ class DatabaseTaskProvider(
// Bind to the service when is starting
bindService()
}
DATABASE_STOP_TASK_ACTION -> {
// Remove the progress task
unBindService()
@@ -331,7 +343,8 @@ class DatabaseTaskProvider(
}
}
}
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
ContextCompat.registerReceiver(
context, databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
@@ -416,47 +429,51 @@ class DatabaseTaskProvider(
----
*/
fun startDatabaseCreate(databaseUri: Uri,
fun startDatabaseCreate(
databaseUri: Uri,
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_CREATE_TASK)
}, ACTION_DATABASE_CREATE_TASK)
}
fun startDatabaseLoad(databaseUri: Uri,
fun startDatabaseLoad(
databaseUri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) {
fixDuplicateUuid: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
putParcelable(
DatabaseTaskNotificationService.CIPHER_DATABASE_KEY,
cipherEncryptDatabase
)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_LOAD_TASK)
}, ACTION_DATABASE_LOAD_TASK)
}
fun startDatabaseMerge(save: Boolean,
fun startDatabaseMerge(
save: Boolean,
fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null) {
mainCredential: MainCredential? = null
) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_MERGE_TASK)
}, ACTION_DATABASE_MERGE_TASK)
}
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_RELOAD_TASK)
}, ACTION_DATABASE_RELOAD_TASK)
}
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
@@ -472,15 +489,15 @@ class DatabaseTaskProvider(
}
}
fun startDatabaseAssignCredential(databaseUri: Uri,
fun startDatabaseAssignCredential(
databaseUri: Uri,
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}
/*
@@ -489,54 +506,60 @@ class DatabaseTaskProvider(
----
*/
fun startDatabaseCreateGroup(newGroup: Group,
fun startDatabaseCreateGroup(
newGroup: Group,
parent: Group,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_CREATE_GROUP_TASK)
}, ACTION_DATABASE_CREATE_GROUP_TASK)
}
fun startDatabaseUpdateGroup(oldGroup: Group,
fun startDatabaseUpdateGroup(
oldGroup: Group,
groupToUpdate: Group,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_GROUP_TASK)
}, ACTION_DATABASE_UPDATE_GROUP_TASK)
}
fun startDatabaseCreateEntry(newEntry: Entry,
fun startDatabaseCreateEntry(
newEntry: Entry,
parent: Group,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_CREATE_ENTRY_TASK)
}, ACTION_DATABASE_CREATE_ENTRY_TASK)
}
fun startDatabaseUpdateEntry(oldEntry: Entry,
fun startDatabaseUpdateEntry(
oldEntry: Entry,
entryToUpdate: Entry,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}
private fun startDatabaseActionListNodes(actionTask: String,
private fun startDatabaseActionListNodes(
actionTask: String,
nodesPaste: List<Node>,
newParent: Group?,
save: Boolean) {
save: Boolean
) {
val groupsIdToCopy = ArrayList<NodeId<*>>()
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
nodesPaste.forEach { nodeVersioned ->
@@ -544,6 +567,7 @@ class DatabaseTaskProvider(
Type.GROUP -> {
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
}
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
}
@@ -558,24 +582,29 @@ class DatabaseTaskProvider(
if (newParentId != null)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, actionTask)
}, actionTask)
}
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
fun startDatabaseCopyNodes(
nodesToCopy: List<Node>,
newParent: Group,
save: Boolean) {
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
}
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
fun startDatabaseMoveNodes(
nodesToMove: List<Node>,
newParent: Group,
save: Boolean) {
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
}
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
save: Boolean) {
fun startDatabaseDeleteNodes(
nodesToDelete: List<Node>,
save: Boolean
) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
}
@@ -585,26 +614,28 @@ class DatabaseTaskProvider(
-----------------
*/
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
fun startDatabaseRestoreEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
fun startDatabaseDeleteEntryHistory(
mainEntryId: NodeId<UUID>,
entryHistoryPosition: Int,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}
/*
@@ -613,110 +644,118 @@ class DatabaseTaskProvider(
-----------------
*/
fun startDatabaseSaveName(oldName: String,
fun startDatabaseSaveName(
oldName: String,
newName: String,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_NAME_TASK)
}, ACTION_DATABASE_UPDATE_NAME_TASK)
}
fun startDatabaseSaveDescription(oldDescription: String,
fun startDatabaseSaveDescription(
oldDescription: String,
newDescription: String,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
fun startDatabaseSaveDefaultUsername(
oldDefaultUsername: String,
newDefaultUsername: String,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}
fun startDatabaseSaveColor(oldColor: String,
fun startDatabaseSaveColor(
oldColor: String,
newColor: String,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_COLOR_TASK)
}, ACTION_DATABASE_UPDATE_COLOR_TASK)
}
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
fun startDatabaseSaveCompression(
oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
fun startDatabaseSaveRecycleBin(
oldRecycleBin: Group?,
newRecycleBin: Group?,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
fun startDatabaseSaveTemplatesGroup(
oldTemplatesGroup: Group?,
newTemplatesGroup: Group?,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
fun startDatabaseSaveMaxHistoryItems(
oldMaxHistoryItems: Int,
newMaxHistoryItems: Int,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
fun startDatabaseSaveMaxHistorySize(
oldMaxHistorySize: Long,
newMaxHistorySize: Long,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}
/*
@@ -725,59 +764,64 @@ class DatabaseTaskProvider(
-------------------
*/
fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
fun startDatabaseSaveEncryption(
oldEncryption: EncryptionAlgorithm,
newEncryption: EncryptionAlgorithm,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
fun startDatabaseSaveKeyDerivation(
oldKeyDerivation: KdfEngine,
newKeyDerivation: KdfEngine,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}
fun startDatabaseSaveIterations(oldIterations: Long,
fun startDatabaseSaveIterations(
oldIterations: Long,
newIterations: Long,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
fun startDatabaseSaveMemoryUsage(
oldMemoryUsage: Long,
newMemoryUsage: Long,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}
fun startDatabaseSaveParallelism(oldParallelism: Long,
fun startDatabaseSaveParallelism(
oldParallelism: Long,
newParallelism: Long,
save: Boolean) {
save: Boolean
) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
}
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}
/**
@@ -787,15 +831,13 @@ class DatabaseTaskProvider(
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
}
, ACTION_DATABASE_SAVE)
}, ACTION_DATABASE_SAVE)
}
fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
}
, ACTION_CHALLENGE_RESPONDED)
}, ACTION_CHALLENGE_RESPONDED)
}
companion object {

View File

@@ -734,7 +734,21 @@
<string name="hide_expired_entries_title">Hide expired entries</string>
<string name="hide_expired_entries_summary">Expired entries are not shown</string>
<string name="passkey_biometric_prompt_title">Confirm passkey usage</string>
<string name="passkey_biometric_prompt_subtitle">for %1$s</string>
<string name="passkey_biometric_prompt_negative_button_text">Cancel</string>
<string name="passkey_usage_biometric_prompt_title">Confirm passkey usage</string>
<string name="passkey_usage_biometric_prompt_subtitle">for %1$s</string>
<string name="passkey_usage_biometric_prompt_negative_button_text">Cancel</string>
<string name="passkey_creation_biometric_prompt_title">Confirm passkey creation</string>
<string name="passkey_creation_biometric_prompt_subtitle">for %1$s</string>
<string name="passkey_creation_biometric_prompt_negative_button_text">Cancel</string>
<string name="passkey_creation_description">Save passkey in new entry</string>
<string name="passkey_update_description">Update passkey in "%1$s"</string>
<string name="passkey_usage_unlock_database_message">Please unlock your database</string>
<string name="passkey_unknown_username">unknown</string>
</resources>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
<credential-provider>
<capabilities>
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
</capabilities>
</credential-provider>

View File

@@ -36,11 +36,17 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
packaging {
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // bouncycastle need this
}
}
dependencies {
// Crypto
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
testImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation 'org.testng:testng:6.9.6'
}

View File

@@ -0,0 +1,102 @@
package com.kunzisoft.asymmetric
import org.junit.Test
class SignatureTest {
// All private keys are for testing only.
// DO NOT USE THEM
private val es256PemInKeypassXC =
"""
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaIrmuL+0IpvMpZ4O
8+CpXEzVNoyNkhquyRqD8CtVWDmhRANCAARyucecj8E9YvcAZHEYgElcLjwLMWmM
vQ2BDZPVL4pLG1oBZer1mPEEQV7LzwGYvTzV/eb9GlXPwj/4la/bpVp1
-----END PRIVATE KEY-----
""".trimIndent().trim()
private val es256PemInKeypassDX = """
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgaIrmuL+0IpvMpZ4O
8+CpXEzVNoyNkhquyRqD8CtVWDmgCgYIKoZIzj0DAQehRANCAARyucecj8E9YvcA
ZHEYgElcLjwLMWmMvQ2BDZPVL4pLG1oBZer1mPEEQV7LzwGYvTzV/eb9GlXPwj/4
la/bpVp1
-----END PRIVATE KEY-----
""".trimIndent().trim()
@Test
fun testEC256KeyConversionKeypassXCIn() {
val privateKey = Signature.createPrivateKey(es256PemInKeypassXC)
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
assert(pemOut == es256PemInKeypassDX)
}
@Test
fun testEC256KeyConversionKeypassDXIn() {
val privateKey = Signature.createPrivateKey(es256PemInKeypassDX)
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
assert(pemOut == es256PemInKeypassDX)
}
@Test
fun testEC256KeyGenAndConversion() {
val (keyPair, keyTypeId) = Signature.generateKeyPair(listOf(Signature.ES256_ALGORITHM))!!
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
assert(keyTypeId == Signature.ES256_ALGORITHM)
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
}
private val rsa256PemIn = """
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaunVJEhLHl7/f
NZufOmj4MY/1J/YHgMAZYFBORQVm58psUjCU7jIww+BK5aRGShdumRzbxr1Yqyh6
yvWbv2u9l6cOdtQKFtXDsWtP9tMBqqhODhG30gE3rEt5l2k1CSzSO9sGghUxlb2i
Q9fiSQ4HmiEc+cXbsSbeYsWwGYNYNhPdJ7vwsZsXzmD0RPpcxy0uJatASjWx3lXF
+eprpcZUr0NLlGIob6VfCt0q2ZfoeEHcEcp4qQ9nI+hOLPFzp/x1TFLX8wKvmwRh
ifwyAauVIaDZaQvAeoBuV2hSBl596ujiJt2Kd3pbQ4SjD1MXudbVvGkofgSZNR0f
ai6f6POFAgMBAAECggEAFtSIVb3K85RahU7dpYLy1hxKB3xb+wNuVNA3STU59NMi
tRTzgiYbVcKxJ5v2v0BTcMg6z9rlOV4X3PZxgwedmB32UlYKN2rjI7rcALKEs+xA
ZTQCPUNJVrOfd1N1/JNb/7FBQhaTlftoPbcQ9Zyd61U8qY/ZN+9NsuaUEMXS8YLe
cqlwJjRcWh3PuTQ+qeVw5l6lgK4XEyDbh/Aj9DGgwVsAkwGdXpuQRBQr8UClO/he
2iOwkn4LJ5nnXwByMpEct03+eUj0kxlijunYbBnKJfRv6tz+ZcpZoc1EqeWbrTB0
eKf+R6N8MHgJSemVVGZvgsUYbfMqkJA/LNOyIpQ/wQKBgQDJhowHVDtze+FQrunR
NchOXgZNWTFf3ZITxxnWnTgumtKdg3MkxeKEBCzAqefb6n6zi2rQzP/PAZAKT/we
YP24hwUVeFePH9/Llf5QuCOWGtkZbNRFSCHWcbfRQAL4vfPJk79bhwCoC5wQ5uk1
atCA+dln6b3wDXq2bvBs6Rj7bQKBgQDEjZIMMgYoEq6yKCFK+11BFo3sj3rmbCcE
tu29mXBfromgzfL0NLoqUAB5OsYKO1nl7eQp2QVIgdLLs8BwTKkel4gosK9B5T4C
umFG0yGIOJz7twA5joLuZAFsoPazj6yHUXaFJaye2P5KwVCL9ws5V2WKgnsF/hKe
QWwSIjtxeQKBgAbyR1NdWOtDIuIYFWErvGrPHOJ/p48JYSajX0Whh7U7ivT4+fgT
hhpM1ooRkTdoXtOrg5QM7OhiwmdImIUnjLdWmBtEWahKTfmDgw+fOULMTB1vPeXh
daEhrFdfIHsYeRXCrP7nqWMhe1Ct1O4Nb4BynEbTrMNgg5FUQ59NbZoFAoGAXNNb
YSUS4UQJexwWtRnHbeDgABO3ADGdr81QtBVOC/IbD3WUQx7PuQH1Z0uJkfV7vGpA
Mj9LDnY5fniS7rZVvJvl8wmWi3FfetxY6qD1mibahMplcclLLpjOT2YpfJ3i5jlj
1vf28UIbvmRTzPZMN7V9wA9lWGwokNLm3h2Ko0kCgYBq0NEd+VMkuIXuGz6j2IXC
qjKf187RZAn2B7otXoumCze+uxm4N0PyOYb1fNGHeE8/RjNQO7VmzZg/dMrk9ZJh
ueHJgOLTbDdlQCUacSipHGmWMN9E+EjgBRiqmPZzV6dq/kGc2FUSGB22wY8gckEX
AmqgkPgYHZ/VzFPTrp97IQ==
-----END PRIVATE KEY-----
""".trimIndent().trim()
@Test
fun testRS256KeyConversion() {
val privateKey = Signature.createPrivateKey(rsa256PemIn)
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
assert(pemOut == rsa256PemIn)
}
@Test
fun testRS256KeyGenAndConversion() {
val (keyPair, keyTypeId) = Signature.generateKeyPair(listOf(Signature.RS256_ALGORITHM))!!
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
assert(keyTypeId == Signature.RS256_ALGORITHM)
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
}
}

View File

@@ -0,0 +1,176 @@
package com.kunzisoft.asymmetric
import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
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
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"
else -> null
}
if (algorithmSignature == null) {
Log.e(this::class.java.simpleName, "sign: privateKeyPem has an unknown algorithm")
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 {
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)
}
}
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
}
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found")
return null
}
fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map<Int, Any>? {
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[1] = es256KeyTypeId
publicKeyMap[3] = 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[1] = rs256KeyTypeId
publicKeyMap[3] = RS256_ALGORITHM
publicKeyMap[-1] =
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
publicKeyMap[-2] =
BigIntegers.asUnsignedByteArray(
rs256ExponentSizeInBytes,
publicKeyIn.publicExponent
)
return publicKeyMap
}
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null
}
}

View File

@@ -0,0 +1,21 @@
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

@@ -1,43 +0,0 @@
package com.kunzisoft.signature
import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.io.StringReader
import java.security.PrivateKey
import java.security.Security
import java.security.Signature
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
object Signature {
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"
else -> "no signature algorithms known"
}
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
sig.initSign(privateKey)
sig.update(message)
return sig.sign()
}
private fun createPrivateKey(privateKeyPem: String): PrivateKey {
val targetReader = StringReader(privateKeyPem);
val a = PEMParser(targetReader)
val privateKeyInfo = a.readObject() as PrivateKeyInfo
val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
return privateKey
}
}