mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
implement creation and update of passkeys
This commit is contained in:
23
README.md
23
README.md
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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("/")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.kunzisoft.keepass.credentialprovider.data
|
||||
|
||||
data class PublicKeyCredentialRequestOptions(
|
||||
val relyingParty: String,
|
||||
val challengeString: String
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
176
crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt
Normal file
176
crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
21
crypto/src/main/java/com/kunzisoft/random/KeePassDXRandom.kt
Normal file
21
crypto/src/main/java/com/kunzisoft/random/KeePassDXRandom.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user