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
|
## 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
|
### requirements
|
||||||
|
|
||||||
- Android 14 or up
|
- Android 14 or up
|
||||||
- enable 3rd party passkeys in chrome. For detail see https://1password.community/discussion/comment/711037/#Comment_711037
|
- 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
|
- set KeepassDX in the Android setting Passwords & Accounts > Your Provider > Enable
|
||||||
|
- biometric authentication set up
|
||||||
|
|
||||||
### working
|
### working
|
||||||
- sign in with ecdsa/rsa passkeys created by KeepassXC in Chrome. Tested with passkeys.io and webauthn.io.
|
|
||||||
|
|
||||||
### maybe working
|
- sign in with ecdsa/rsa passkeys created by KeepassXC or KeepassDX in Chrome/Firefox. Tested with
|
||||||
- sign in with passkeys apps natively (without browser)
|
passkeys.io and webauthn.io.
|
||||||
|
- create new passkeys with ecdsa/rsa in root group (compatible with KeepassXC)
|
||||||
|
- update existing passkeys
|
||||||
|
|
||||||
### not working
|
### not working
|
||||||
- create passkeys
|
|
||||||
- user credential provider with username/password
|
- support for username/password see provider.xml
|
||||||
- open KeepassDX to unlock the database, if it is locked (currently a dummy entry with title unlock db is shown)
|
- 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:largeHeap="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="s"
|
tools:targetApi="tiramisu"
|
||||||
android:enableOnBackInvokedCallback="true">
|
android:enableOnBackInvokedCallback="true">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -197,17 +197,21 @@
|
|||||||
android:label="@string/keyboard_setting_label"
|
android:label="@string/keyboard_setting_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.kunzisoft.keepass.credentialprovider.CredentialProviderActivity"
|
<activity
|
||||||
android:label="CredentialProviderActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.CreatePasskeyActivity"
|
||||||
android:exported="true">
|
android:label="CreatePasskeyActivity"
|
||||||
<intent-filter>
|
android:exported="true"
|
||||||
<action android:name="com.kunzisoft.keepass.credentialprovider.GET_PASSKEY"/>
|
tools:targetApi="upside_down_cake" />
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.UsePasskeyActivity"
|
||||||
|
android:label="UsePasskeyActivity"
|
||||||
|
android:exported="true"
|
||||||
|
tools:targetApi="upside_down_cake" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
@@ -259,18 +263,20 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name="com.kunzisoft.keepass.credentialprovider.KeePassDXCredentialProviderService"
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.service.KeePassDXCredentialProviderService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="KeyPassDX Credential Provider"
|
android:label="KeyPassDX Credential Provider"
|
||||||
android:icon="@mipmap/ic_launcher"
|
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>
|
<intent-filter>
|
||||||
<action android:name="android.service.credentials.CredentialProviderService"/>
|
<action android:name="android.service.credentials.CredentialProviderService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.credentials.provider"
|
android:name="android.credentials.provider"
|
||||||
android:resource="@xml/provider"/>
|
android:resource="@xml/provider" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import android.os.Bundle
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
@@ -41,6 +41,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
mDatabaseTaskProvider?.destroy()
|
mDatabaseTaskProvider?.destroy()
|
||||||
mDatabaseTaskProvider = null
|
mDatabaseTaskProvider = null
|
||||||
@@ -48,6 +49,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
mDatabase = database
|
mDatabase = database
|
||||||
mDatabaseViewModel.defineDatabase(database)
|
mDatabaseViewModel.defineDatabase(database)
|
||||||
@@ -77,7 +79,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUuid: Boolean
|
fixDuplicateUuid: Boolean
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
mDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUuid
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
protected fun closeDatabase() {
|
||||||
@@ -88,7 +96,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
mDatabaseTaskProvider?.registerProgressTask()
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|||||||
@@ -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
|
// To show dialog only if context is an activity
|
||||||
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
private var activity: FragmentActivity? = try {
|
||||||
catch (_: Exception) { null }
|
context as? FragmentActivity?
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||||
|
|
||||||
var onActionFinish: ((database: ContextualDatabase,
|
var onActionFinish: ((
|
||||||
actionTask: String,
|
database: ContextualDatabase,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask: Intent = Intent(
|
private var intentDatabaseTask: Intent = Intent(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
@@ -141,7 +146,7 @@ class DatabaseTaskProvider(
|
|||||||
this.databaseChangedDialogFragment = null
|
this.databaseChangedDialogFragment = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onActionStarted(
|
override fun onActionStarted(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
progressMessage: ProgressMessage
|
progressMessage: ProgressMessage
|
||||||
@@ -175,13 +180,14 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
private val mActionDatabaseListener =
|
||||||
override fun validateDatabaseChanged() {
|
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
mBinder?.getService()?.saveDatabaseInfo()
|
override fun validateDatabaseChanged() {
|
||||||
|
mBinder?.getService()?.saveDatabaseInfo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var databaseInfoListener = object:
|
private var databaseInfoListener = object :
|
||||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
override fun onDatabaseInfoChanged(
|
override fun onDatabaseInfoChanged(
|
||||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
@@ -214,7 +220,7 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
onDatabaseRetrieved?.invoke(database)
|
onDatabaseRetrieved?.invoke(database)
|
||||||
}
|
}
|
||||||
@@ -265,12 +271,13 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
mBinder =
|
||||||
addServiceListeners(this)
|
(serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||||
getService().checkDatabase()
|
addServiceListeners(this)
|
||||||
getService().checkDatabaseInfo()
|
getService().checkDatabase()
|
||||||
getService().checkAction()
|
getService().checkDatabaseInfo()
|
||||||
}
|
getService().checkAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
@@ -296,7 +303,11 @@ class DatabaseTaskProvider(
|
|||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
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
|
// Bind to the service when is starting
|
||||||
bindService()
|
bindService()
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASE_STOP_TASK_ACTION -> {
|
DATABASE_STOP_TASK_ACTION -> {
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
unBindService()
|
unBindService()
|
||||||
@@ -331,7 +343,8 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
|
ContextCompat.registerReceiver(
|
||||||
|
context, databaseTaskBroadcastReceiver,
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(DATABASE_START_TASK_ACTION)
|
addAction(DATABASE_START_TASK_ACTION)
|
||||||
addAction(DATABASE_STOP_TASK_ACTION)
|
addAction(DATABASE_STOP_TASK_ACTION)
|
||||||
@@ -416,47 +429,51 @@ class DatabaseTaskProvider(
|
|||||||
----
|
----
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreate(databaseUri: Uri,
|
fun startDatabaseCreate(
|
||||||
mainCredential: MainCredential
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_TASK)
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(
|
||||||
mainCredential: MainCredential,
|
databaseUri: Uri,
|
||||||
readOnly: Boolean,
|
mainCredential: MainCredential,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
readOnly: Boolean,
|
||||||
fixDuplicateUuid: Boolean) {
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUuid: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
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)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}, ACTION_DATABASE_LOAD_TASK)
|
||||||
, ACTION_DATABASE_LOAD_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMerge(save: Boolean,
|
fun startDatabaseMerge(
|
||||||
fromDatabaseUri: Uri? = null,
|
save: Boolean,
|
||||||
mainCredential: MainCredential? = null) {
|
fromDatabaseUri: Uri? = null,
|
||||||
|
mainCredential: MainCredential? = null
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_MERGE_TASK)
|
||||||
, ACTION_DATABASE_MERGE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}, ACTION_DATABASE_RELOAD_TASK)
|
||||||
, ACTION_DATABASE_RELOAD_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||||
@@ -472,15 +489,15 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignCredential(databaseUri: Uri,
|
fun startDatabaseAssignCredential(
|
||||||
mainCredential: MainCredential
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
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(
|
||||||
parent: Group,
|
newGroup: Group,
|
||||||
save: Boolean) {
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateGroup(oldGroup: Group,
|
fun startDatabaseUpdateGroup(
|
||||||
groupToUpdate: Group,
|
oldGroup: Group,
|
||||||
save: Boolean) {
|
groupToUpdate: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCreateEntry(newEntry: Entry,
|
fun startDatabaseCreateEntry(
|
||||||
parent: Group,
|
newEntry: Entry,
|
||||||
save: Boolean) {
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
fun startDatabaseUpdateEntry(
|
||||||
entryToUpdate: Entry,
|
oldEntry: Entry,
|
||||||
save: Boolean) {
|
entryToUpdate: Entry,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDatabaseActionListNodes(actionTask: String,
|
private fun startDatabaseActionListNodes(
|
||||||
nodesPaste: List<Node>,
|
actionTask: String,
|
||||||
newParent: Group?,
|
nodesPaste: List<Node>,
|
||||||
save: Boolean) {
|
newParent: Group?,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
||||||
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
||||||
nodesPaste.forEach { nodeVersioned ->
|
nodesPaste.forEach { nodeVersioned ->
|
||||||
@@ -544,6 +567,7 @@ class DatabaseTaskProvider(
|
|||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Type.ENTRY -> {
|
Type.ENTRY -> {
|
||||||
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
||||||
}
|
}
|
||||||
@@ -558,24 +582,29 @@ class DatabaseTaskProvider(
|
|||||||
if (newParentId != null)
|
if (newParentId != null)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, actionTask)
|
||||||
, actionTask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
fun startDatabaseCopyNodes(
|
||||||
newParent: Group,
|
nodesToCopy: List<Node>,
|
||||||
save: Boolean) {
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
|
fun startDatabaseMoveNodes(
|
||||||
newParent: Group,
|
nodesToMove: List<Node>,
|
||||||
save: Boolean) {
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
|
fun startDatabaseDeleteNodes(
|
||||||
save: Boolean) {
|
nodesToDelete: List<Node>,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,26 +614,28 @@ class DatabaseTaskProvider(
|
|||||||
-----------------
|
-----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseRestoreEntryHistory(
|
||||||
entryHistoryPosition: Int,
|
mainEntryId: NodeId<UUID>,
|
||||||
save: Boolean) {
|
entryHistoryPosition: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseDeleteEntryHistory(
|
||||||
entryHistoryPosition: Int,
|
mainEntryId: NodeId<UUID>,
|
||||||
save: Boolean) {
|
entryHistoryPosition: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
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(
|
||||||
newName: String,
|
oldName: String,
|
||||||
save: Boolean) {
|
newName: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDescription(oldDescription: String,
|
fun startDatabaseSaveDescription(
|
||||||
newDescription: String,
|
oldDescription: String,
|
||||||
save: Boolean) {
|
newDescription: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
fun startDatabaseSaveDefaultUsername(
|
||||||
newDefaultUsername: String,
|
oldDefaultUsername: String,
|
||||||
save: Boolean) {
|
newDefaultUsername: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveColor(oldColor: String,
|
fun startDatabaseSaveColor(
|
||||||
newColor: String,
|
oldColor: String,
|
||||||
save: Boolean) {
|
newColor: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
fun startDatabaseSaveCompression(
|
||||||
newCompression: CompressionAlgorithm,
|
oldCompression: CompressionAlgorithm,
|
||||||
save: Boolean) {
|
newCompression: CompressionAlgorithm,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
fun startDatabaseSaveRecycleBin(
|
||||||
newRecycleBin: Group?,
|
oldRecycleBin: Group?,
|
||||||
save: Boolean) {
|
newRecycleBin: Group?,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
||||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
|
fun startDatabaseSaveTemplatesGroup(
|
||||||
newTemplatesGroup: Group?,
|
oldTemplatesGroup: Group?,
|
||||||
save: Boolean) {
|
newTemplatesGroup: Group?,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
||||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(
|
||||||
newMaxHistoryItems: Int,
|
oldMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
newMaxHistoryItems: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
||||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
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(
|
||||||
newMaxHistorySize: Long,
|
oldMaxHistorySize: Long,
|
||||||
save: Boolean) {
|
newMaxHistorySize: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
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(
|
||||||
newEncryption: EncryptionAlgorithm,
|
oldEncryption: EncryptionAlgorithm,
|
||||||
save: Boolean) {
|
newEncryption: EncryptionAlgorithm,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
fun startDatabaseSaveKeyDerivation(
|
||||||
newKeyDerivation: KdfEngine,
|
oldKeyDerivation: KdfEngine,
|
||||||
save: Boolean) {
|
newKeyDerivation: KdfEngine,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
fun startDatabaseSaveIterations(
|
||||||
newIterations: Long,
|
oldIterations: Long,
|
||||||
save: Boolean) {
|
newIterations: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
fun startDatabaseSaveMemoryUsage(
|
||||||
newMemoryUsage: Long,
|
oldMemoryUsage: Long,
|
||||||
save: Boolean) {
|
newMemoryUsage: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
fun startDatabaseSaveParallelism(
|
||||||
newParallelism: Long,
|
oldParallelism: Long,
|
||||||
save: Boolean) {
|
newParallelism: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
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 {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||||
}
|
}, ACTION_DATABASE_SAVE)
|
||||||
, ACTION_DATABASE_SAVE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startChallengeResponded(response: ByteArray?) {
|
fun startChallengeResponded(response: ByteArray?) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||||
}
|
}, ACTION_CHALLENGE_RESPONDED)
|
||||||
, ACTION_CHALLENGE_RESPONDED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -734,7 +734,21 @@
|
|||||||
<string name="hide_expired_entries_title">Hide expired entries</string>
|
<string name="hide_expired_entries_title">Hide expired entries</string>
|
||||||
<string name="hide_expired_entries_summary">Expired entries are not shown</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_usage_biometric_prompt_title">Confirm passkey usage</string>
|
||||||
<string name="passkey_biometric_prompt_subtitle">for %1$s</string>
|
<string name="passkey_usage_biometric_prompt_subtitle">for %1$s</string>
|
||||||
<string name="passkey_biometric_prompt_negative_button_text">Cancel</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>
|
</resources>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
|
<credential-provider>
|
||||||
<capabilities>
|
<capabilities>
|
||||||
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
|
|
||||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||||
</capabilities>
|
</capabilities>
|
||||||
</credential-provider>
|
</credential-provider>
|
||||||
|
|||||||
@@ -36,11 +36,17 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // bouncycastle need this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Crypto
|
// Crypto
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
|
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