mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'release/4.1.4'
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
KeePassDX(4.1.4)
|
||||
* Fix UnlockManager #2098 #2101
|
||||
* Auto device unlock prompt #2105
|
||||
* Small fixes ##2066
|
||||
|
||||
KeePassDX(4.1.3)
|
||||
* Fix Autofill Registration #2089
|
||||
* Fix Biometric errors #2081
|
||||
* Fixed timestamp in copy file #1981 #1983
|
||||
* Fix Template Email #1986
|
||||
* Fix Search #2096
|
||||
|
||||
KeePassDX(4.1.2)
|
||||
* Fix URL search #1940 #1946 #2003 #2040 #2044
|
||||
|
||||
75
Gemfile.lock
75
Gemfile.lock
@@ -9,31 +9,35 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1009.0)
|
||||
aws-sdk-core (3.213.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1146.0)
|
||||
aws-sdk-core (3.229.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
logger
|
||||
aws-sdk-kms (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.171.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.196.1)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
@@ -55,11 +59,11 @@ GEM
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
@@ -67,8 +71,8 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.225.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.228.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -108,7 +112,7 @@ GEM
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-versioning_android (0.1.1)
|
||||
fastlane-sirp (1.0.0)
|
||||
@@ -130,12 +134,12 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
@@ -151,36 +155,39 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.7)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.8.2)
|
||||
jwt (2.9.3)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multi_json (1.17.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.1)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.9)
|
||||
rouge (2.0.7)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
signet (0.20.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
@@ -207,8 +214,8 @@ GEM
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
@@ -220,4 +227,4 @@ DEPENDENCIES
|
||||
fastlane-plugin-versioning_android
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.10
|
||||
2.6.9
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 34
|
||||
versionCode = 135
|
||||
versionName = "4.1.3"
|
||||
versionCode = 136
|
||||
versionName = "4.1.4"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -316,6 +316,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
|
||||
@@ -51,19 +51,24 @@ import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
@@ -81,8 +86,9 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockState
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
@@ -98,10 +104,10 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private var confirmButtonView: Button? = null
|
||||
private var infoContainerView: ViewGroup? = null
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||
|
||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels()
|
||||
|
||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||
|
||||
@@ -170,8 +176,9 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability(
|
||||
conditionToStoreCredentialVerified = verified
|
||||
mDeviceUnlockViewModel.checkConditionToStoreCredential(
|
||||
condition = verified,
|
||||
databaseFileUri = mDatabaseFileUri
|
||||
)
|
||||
// TODO Async by ViewModel
|
||||
enableConfirmationButton()
|
||||
@@ -226,20 +233,31 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mAdvancedUnlockViewModel.uiState.collect { uiState ->
|
||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
if (uiState.isCredentialRequired) {
|
||||
mAdvancedUnlockViewModel.provideCredentialForEncryption(
|
||||
getCredentialForEncryption()
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
mDeviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
mAdvancedUnlockViewModel.consumeCredentialEncrypted()
|
||||
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
mAdvancedUnlockViewModel.consumeCredentialDecrypted()
|
||||
mDeviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
mDeviceUnlockViewModel.exceptionShown()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +268,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
super.onResume()
|
||||
|
||||
// Init Biometric elements only if allowed
|
||||
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||
advancedUnlockFragment = supportFragmentManager
|
||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||
if (advancedUnlockFragment == null) {
|
||||
advancedUnlockFragment = AdvancedUnlockFragment().also {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||
deviceUnlockFragment = supportFragmentManager
|
||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||
if (deviceUnlockFragment == null) {
|
||||
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||
supportFragmentManager.commit {
|
||||
replace(
|
||||
R.id.fragment_advanced_unlock_container_view,
|
||||
@@ -274,11 +293,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
}
|
||||
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
@@ -402,6 +416,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
// Check if database really loaded
|
||||
if (database.loaded) {
|
||||
mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true
|
||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
@@ -494,7 +509,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
loadDatabase()
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||
mDeviceUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||
}
|
||||
|
||||
enableConfirmationButton()
|
||||
@@ -524,8 +539,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
|
||||
override fun onPause() {
|
||||
// Reinit locking activity UI variable
|
||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
|
||||
UI_VISIBLE_DURING_LOCK = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -654,7 +668,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !readOnlyEducationPerformed) {
|
||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
||||
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& advancedUnlockButton != null) {
|
||||
|
||||
@@ -47,10 +47,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.LockReceiver
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
PasswordEncodingDialogFragment.Listener {
|
||||
@@ -184,8 +188,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mLockReceiver = LockReceiver {
|
||||
mDatabase = null
|
||||
closeDatabase(database)
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
UI_VISIBLE_DURING_LOCK = UI_VISIBLE
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
@@ -414,7 +417,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||
UI_VISIBLE = true
|
||||
}
|
||||
|
||||
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||
@@ -429,7 +432,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
UI_VISIBLE = false
|
||||
|
||||
super.onPause()
|
||||
|
||||
@@ -481,8 +484,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||
var UI_VISIBLE: Boolean = false
|
||||
var UI_VISIBLE_DURING_LOCK: Boolean = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.stylish
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -77,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
startActivity(intent)
|
||||
}
|
||||
finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
overrideActivityTransition(
|
||||
OVERRIDE_TRANSITION_OPEN,
|
||||
android.R.anim.fade_in,
|
||||
android.R.anim.fade_out
|
||||
)
|
||||
else
|
||||
overridePendingTransition(
|
||||
android.R.anim.fade_in,
|
||||
android.R.anim.fade_out
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -177,14 +177,18 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun containsCipherDatabase(databaseUri: Uri,
|
||||
fun containsCipherDatabase(databaseUri: Uri?,
|
||||
contains: (Boolean) -> Unit) {
|
||||
getCipherDatabase(databaseUri) {
|
||||
contains.invoke(it != null)
|
||||
if (databaseUri == null) {
|
||||
contains.invoke(false)
|
||||
} else {
|
||||
getCipherDatabase(databaseUri) {
|
||||
contains.invoke(it != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCipherParameters(databaseUri: Uri) {
|
||||
fun resetCipherParameters(databaseUri: Uri?) {
|
||||
containsCipherDatabase(databaseUri) { contains ->
|
||||
if (contains) {
|
||||
mBinder?.resetTimer()
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import javax.crypto.Cipher
|
||||
|
||||
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
|
||||
@StringRes var promptTitleId: Int,
|
||||
@StringRes var promptDescriptionId: Int? = null,
|
||||
var isDeviceCredentialOperation: Boolean,
|
||||
var isBiometricOperation: Boolean)
|
||||
@@ -1,667 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||
|
||||
private var mAdvancedUnlockEnabled = false
|
||||
private var mAutoOpenPromptEnabled = false
|
||||
|
||||
private var advancedUnlockManager: AdvancedUnlockManager? = null
|
||||
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
||||
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||
|
||||
var databaseFileUri: Uri? = null
|
||||
private set
|
||||
|
||||
// TODO Retrieve credential storage from app database
|
||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||
|
||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||
private var allowOpenBiometricPrompt = false
|
||||
|
||||
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
||||
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
private var mAddBiometricMenuInProgress = false
|
||||
|
||||
// Only keep connection when we request a device credential activity
|
||||
private var keepConnection = false
|
||||
|
||||
private var isConditionToStoreCredentialVerified = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded =
|
||||
result.resultCode == Activity.RESULT_OK
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// biometric menu
|
||||
if (mAllowAdvancedUnlockMenu)
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||
|
||||
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mAdvancedUnlockViewModel.uiState.collect { uiState ->
|
||||
// Database loaded
|
||||
uiState.databaseFileLoaded?.let { databaseLoaded ->
|
||||
onDatabaseLoaded(databaseLoaded)
|
||||
mAdvancedUnlockViewModel.consumeDatabaseFileLoaded()
|
||||
}
|
||||
// New credential value received
|
||||
uiState.credential?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockManager?.encryptData(uiState.credential)
|
||||
}
|
||||
mAdvancedUnlockViewModel.consumeCredentialForEncryption()
|
||||
}
|
||||
// Condition to store credential verified
|
||||
isConditionToStoreCredentialVerified = uiState.isConditionToStoreCredentialVerified
|
||||
// Check unlock availability
|
||||
if (uiState.onUnlockAvailabilityCheckRequested) {
|
||||
checkUnlockAvailability()
|
||||
mAdvancedUnlockViewModel.consumeCheckUnlockAvailability()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
context?.let {
|
||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
|
||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// To get device credential unlock result, only if same database uri
|
||||
if (databaseUri != null
|
||||
&& mAdvancedUnlockEnabled) {
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded?.let { authSucceeded ->
|
||||
if (databaseUri == databaseFileUri) {
|
||||
if (authSucceeded) {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
} else {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
} ?: run {
|
||||
if (databaseUri != databaseFileUri) {
|
||||
connect(databaseUri)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability and change the current mode depending of device's state
|
||||
*/
|
||||
private fun checkUnlockAvailability() {
|
||||
context?.let { context ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
allowOpenBiometricPrompt = true
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
||||
// biometric not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
|
||||
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||
} else {
|
||||
// biometric is available but not configured, show icon but in disabled state with some information
|
||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
} else {
|
||||
selectMode()
|
||||
}
|
||||
}
|
||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
if (AdvancedUnlockManager.isDeviceSecure(context)) {
|
||||
selectMode()
|
||||
} else {
|
||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun selectMode() {
|
||||
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
||||
// and the activity still active)
|
||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
|
||||
// callback for fingerprint findings
|
||||
advancedUnlockManager?.advancedUnlockCallback = this
|
||||
}
|
||||
// Recheck to change the mode
|
||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
||||
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
||||
} else {
|
||||
if (isConditionToStoreCredentialVerified) {
|
||||
// listen for encryption
|
||||
toggleMode(Mode.STORE_CREDENTIAL)
|
||||
} else {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||
// biometric available but no stored password found yet for this DB so show info don't listen
|
||||
toggleMode(if (containsCipher) {
|
||||
// listen for decryption
|
||||
Mode.EXTRACT_CREDENTIAL
|
||||
} else {
|
||||
if (isConditionToStoreCredentialVerified) {
|
||||
// if condition OK, key manager in error
|
||||
Mode.KEY_MANAGER_UNAVAILABLE
|
||||
} else {
|
||||
// wait for typing
|
||||
Mode.WAIT_CREDENTIAL
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun toggleMode(newBiometricMode: Mode) {
|
||||
if (newBiometricMode != biometricMode) {
|
||||
biometricMode = newBiometricMode
|
||||
initAdvancedUnlockMode()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initNotAvailable() {
|
||||
showViews(false)
|
||||
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun openBiometricSetting() {
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
||||
try {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||
@Suppress("DEPRECATION") context
|
||||
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
|
||||
}
|
||||
else -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initSecurityUpdateRequired() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initNotConfigured() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initKeyManagerNotAvailable() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initWaitData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unavailable)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
context?.let { context ->
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
||||
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (allowOpenBiometricPrompt) {
|
||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||
keepConnection = true
|
||||
try {
|
||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
||||
mDeviceCredentialResultLauncher)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initEncryptData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||
// Set listener to open the biometric dialog and save credential
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initDecryptData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockManager?.let { unlockHelper ->
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||
cipherDatabase?.let {
|
||||
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
|
||||
|
||||
// Set listener to open the biometric dialog and check credential
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
|
||||
// Auto open the biometric prompt
|
||||
if (mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt
|
||||
&& mAutoOpenPromptEnabled) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: throw UnknownDatabaseLocationException()
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
private fun initAdvancedUnlockMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
try {
|
||||
when (biometricMode) {
|
||||
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
||||
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onGenericException(e)
|
||||
}
|
||||
invalidateBiometricMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateBiometricMenu() {
|
||||
// Show fingerprint key deletion
|
||||
if (!mAddBiometricMenuInProgress) {
|
||||
mAddBiometricMenuInProgress = true
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||
mAllowAdvancedUnlockMenu = containsCipher
|
||||
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
||||
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
||||
mAddBiometricMenuInProgress = false
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun connect(databaseUri: Uri) {
|
||||
showViews(true)
|
||||
this.databaseFileUri = databaseUri
|
||||
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||
override fun onCipherDatabaseCleared() {
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
}
|
||||
cipherDatabaseAction.apply {
|
||||
reloadPreferences()
|
||||
cipherDatabaseListener?.let {
|
||||
registerDatabaseListener(it)
|
||||
}
|
||||
}
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun disconnect(hideViews: Boolean = true,
|
||||
closePrompt: Boolean = true) {
|
||||
this.databaseFileUri = null
|
||||
// Close the biometric prompt
|
||||
allowOpenBiometricPrompt = false
|
||||
if (closePrompt)
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
cipherDatabaseListener?.let {
|
||||
cipherDatabaseAction.unregisterDatabaseListener(it)
|
||||
}
|
||||
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
|
||||
if (hideViews) {
|
||||
showViews(false)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
} ?: checkUnlockAvailability()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||
setAdvancedUnlockedMessageView(errString.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onAuthenticationFailed() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onAuthenticationSucceeded() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
when (biometricMode) {
|
||||
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||
}
|
||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
||||
}
|
||||
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
|
||||
}
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
||||
}
|
||||
Mode.WAIT_CREDENTIAL -> {
|
||||
}
|
||||
Mode.STORE_CREDENTIAL -> {
|
||||
// newly store the entered password in encrypted way
|
||||
mAdvancedUnlockViewModel.retrieveCredentialForEncryption()
|
||||
}
|
||||
Mode.EXTRACT_CREDENTIAL -> {
|
||||
// retrieve the encrypted value from preferences
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||
cipherDatabase?.encryptedValue?.let { value ->
|
||||
advancedUnlockManager?.decryptData(value)
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: run {
|
||||
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
mAdvancedUnlockViewModel.onCredentialEncrypted(
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.encryptedValue = encryptedValue
|
||||
this.specParameters = ivSpec
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
||||
// Load database directly with password retrieve
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
mAdvancedUnlockViewModel.onCredentialDecrypted(
|
||||
CipherDecryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.decryptedValue = decryptedValue
|
||||
}
|
||||
)
|
||||
cipherDatabaseAction.resetCipherParameters(databaseUri)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onGenericException(e: Exception) {
|
||||
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
||||
setAdvancedUnlockedMessageView(errorMessage)
|
||||
}
|
||||
|
||||
private fun showViews(show: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (show) {
|
||||
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
|
||||
mAdvancedUnlockInfoView?.showByFading()
|
||||
}
|
||||
else {
|
||||
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
|
||||
mAdvancedUnlockInfoView?.hideByFading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.setTitle(textId)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.setMessage(textId)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.setMessage(text)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
BIOMETRIC_UNAVAILABLE,
|
||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
||||
KEY_MANAGER_UNAVAILABLE,
|
||||
WAIT_CREDENTIAL,
|
||||
STORE_CREDENTIAL,
|
||||
EXTRACT_CREDENTIAL
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!keepConnection) {
|
||||
// If close prompt, bug "user not authenticated in Android R"
|
||||
disconnect(false)
|
||||
advancedUnlockManager = null
|
||||
}
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
mAdvancedUnlockInfoView = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
disconnect()
|
||||
advancedUnlockManager = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = AdvancedUnlockFragment::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.util.concurrent.Executors
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
|
||||
|
||||
private var keyStore: KeyStore? = null
|
||||
private var keyGenerator: KeyGenerator? = null
|
||||
private var cipher: Cipher? = null
|
||||
|
||||
private var biometricPrompt: BiometricPrompt? = null
|
||||
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
|
||||
}
|
||||
}
|
||||
|
||||
var advancedUnlockCallback: AdvancedUnlockCallback? = null
|
||||
|
||||
private var isKeyManagerInit = false
|
||||
|
||||
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
|
||||
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
|
||||
|
||||
val isKeyManagerInitialized: Boolean
|
||||
get() {
|
||||
if (!isKeyManagerInit) {
|
||||
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
|
||||
}
|
||||
return isKeyManagerInit
|
||||
}
|
||||
|
||||
private fun isBiometricOperation(): Boolean {
|
||||
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
|
||||
}
|
||||
|
||||
// Since Android 30, device credential is also a biometric operation
|
||||
private fun isDeviceCredentialOperation(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
private fun isDeviceCredentialBiometricOperation(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
init {
|
||||
if (isDeviceSecure(retrieveContext())
|
||||
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
||||
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
|
||||
this.cipher = Cipher.getInstance(
|
||||
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
||||
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
||||
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||
isKeyManagerInit = (keyStore != null
|
||||
&& keyGenerator != null
|
||||
&& cipher != null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize the keystore", e)
|
||||
isKeyManagerInit = false
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
} else {
|
||||
// really not much to do when no fingerprint support found
|
||||
isKeyManagerInit = false
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
// Create new key if needed
|
||||
keyStore?.let { keyStore ->
|
||||
keyStore.load(null)
|
||||
|
||||
try {
|
||||
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator?.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
// To store in the security chip
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& retrieveContext().packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
|
||||
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||
initEncryptData(actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
|
||||
actionIfCypherInit.invoke(
|
||||
AdvancedUnlockCryptoPrompt(
|
||||
cipher,
|
||||
R.string.advanced_unlock_prompt_store_credential_title,
|
||||
R.string.advanced_unlock_prompt_store_credential_message,
|
||||
isDeviceCredentialOperation(), isBiometricOperation())
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initEncryptData(actionIfCypherInit, false)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun encryptData(value: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
||||
// passes updated iv spec on to callback so this can be stored for decryption
|
||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to encrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean = true) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// important to restore spec here that was used for decryption
|
||||
val spec = IvParameterSpec(ivSpecValue)
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
actionIfCypherInit.invoke(
|
||||
AdvancedUnlockCryptoPrompt(
|
||||
cipher,
|
||||
R.string.advanced_unlock_prompt_extract_credential_title,
|
||||
null,
|
||||
isDeviceCredentialOperation(), isBiometricOperation())
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteKeystoreKey()
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
}
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun decryptData(encryptedValue: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// actual decryption here
|
||||
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||
advancedUnlockCallback?.handleDecryptedResult(decrypted)
|
||||
}
|
||||
} catch (badPaddingException: BadPaddingException) {
|
||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun deleteKeystoreKey() {
|
||||
try {
|
||||
keyStore?.load(null)
|
||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
// Init advanced unlock prompt
|
||||
if (biometricPrompt == null) {
|
||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||
Executors.newSingleThreadExecutor(),
|
||||
authenticationCallback)
|
||||
}
|
||||
|
||||
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
|
||||
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
|
||||
retrieveContext().getString(descriptionId)
|
||||
} ?: ""
|
||||
|
||||
if (cryptoPrompt.isBiometricOperation) {
|
||||
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(promptTitle)
|
||||
if (promptDescription.isNotEmpty())
|
||||
setDescription(promptDescription)
|
||||
setConfirmationRequired(false)
|
||||
if (isDeviceCredentialBiometricOperation()) {
|
||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||
} else {
|
||||
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
|
||||
}
|
||||
}.build()
|
||||
biometricPrompt?.authenticate(
|
||||
promptInfoExtractCredential,
|
||||
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
||||
}
|
||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||
@Suppress("DEPRECATION")
|
||||
deviceCredentialResultLauncher.launch(
|
||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun closeBiometricPrompt() {
|
||||
biometricPrompt?.cancelAuthentication()
|
||||
}
|
||||
|
||||
interface AdvancedUnlockErrorCallback {
|
||||
fun onUnrecoverableKeyException(e: Exception)
|
||||
fun onInvalidKeyException(e: Exception)
|
||||
fun onGenericException(e: Exception)
|
||||
}
|
||||
|
||||
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
|
||||
fun onAuthenticationSucceeded()
|
||||
fun onAuthenticationFailed()
|
||||
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
|
||||
fun handleDecryptedResult(decryptedValue: ByteArray)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = AdvancedUnlockManager::class.java.name
|
||||
|
||||
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_STRONG
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_WEAK
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDeviceSecure(context: Context): Boolean {
|
||||
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
?.isDeviceSecure ?: false
|
||||
}
|
||||
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
}
|
||||
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
|
||||
advancedCallback: AdvancedUnlockErrorCallback) {
|
||||
AdvancedUnlockManager{ fragmentActivity }.apply {
|
||||
advancedUnlockCallback = object : AdvancedUnlockCallback {
|
||||
override fun onAuthenticationSucceeded() {}
|
||||
|
||||
override fun onAuthenticationFailed() {}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
advancedCallback.onUnrecoverableKeyException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
advancedCallback.onInvalidKeyException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
advancedCallback.onGenericException(e)
|
||||
}
|
||||
}
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : AdvancedUnlockErrorCallback {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(activity,
|
||||
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import javax.crypto.Cipher
|
||||
|
||||
data class DeviceUnlockCryptoPrompt(
|
||||
var type: DeviceUnlockCryptoPromptType,
|
||||
var cipher: Cipher,
|
||||
@StringRes var titleId: Int,
|
||||
@StringRes var descriptionId: Int? = null,
|
||||
var isDeviceCredentialOperation: Boolean,
|
||||
var isBiometricOperation: Boolean
|
||||
)
|
||||
|
||||
enum class DeviceUnlockCryptoPromptType {
|
||||
CREDENTIAL_ENCRYPTION, CREDENTIAL_DECRYPTION
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK
|
||||
import com.kunzisoft.keepass.view.DeviceUnlockView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockPromptMode
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class DeviceUnlockFragment: Fragment() {
|
||||
|
||||
private var mDeviceUnlockView: DeviceUnlockView? = null
|
||||
|
||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by activityViewModels()
|
||||
|
||||
private var mBiometricPrompt: BiometricPrompt? = null
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
|
||||
} else {
|
||||
setAuthenticationFailed()
|
||||
}
|
||||
}
|
||||
|
||||
private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
setAuthenticationFailed()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
setAuthenticationError(errorCode, errString)
|
||||
}
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
// biometric menu
|
||||
if (mAllowAdvancedUnlockMenu)
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_keystore_remove_key ->
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||
|
||||
mDeviceUnlockView = rootView.findViewById(R.id.advanced_unlock_view)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Init device unlock prompt
|
||||
mBiometricPrompt = BiometricPrompt(
|
||||
this@DeviceUnlockFragment,
|
||||
Executors.newSingleThreadExecutor(),
|
||||
biometricAuthenticationCallback
|
||||
)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// Change mode
|
||||
toggleDeviceCredentialMode(uiState.newDeviceUnlockMode)
|
||||
// Prompt
|
||||
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
|
||||
// Advanced menu
|
||||
mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
if (UI_VISIBLE_DURING_LOCK) {
|
||||
mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
}
|
||||
|
||||
mDeviceUnlockViewModel.checkUnlockAvailability()
|
||||
}
|
||||
|
||||
fun cancelBiometricPrompt() {
|
||||
mBiometricPrompt?.cancelAuthentication()
|
||||
}
|
||||
|
||||
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||
try {
|
||||
when (deviceUnlockMode) {
|
||||
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
|
||||
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
|
||||
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
|
||||
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
|
||||
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
|
||||
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
|
||||
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDeviceUnlockViewModel.setException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun manageDeviceCredentialPrompt(
|
||||
state: DeviceUnlockPromptMode
|
||||
) {
|
||||
mDeviceUnlockViewModel.cryptoPrompt?.let { prompt ->
|
||||
when (state) {
|
||||
DeviceUnlockPromptMode.IDLE -> {}
|
||||
DeviceUnlockPromptMode.SHOW -> {
|
||||
openPrompt(prompt)
|
||||
mDeviceUnlockViewModel.promptShown()
|
||||
}
|
||||
DeviceUnlockPromptMode.CLOSE -> {
|
||||
cancelBiometricPrompt()
|
||||
mDeviceUnlockViewModel.biometricPromptClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
|
||||
try {
|
||||
val promptTitle = getString(cryptoPrompt.titleId)
|
||||
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
|
||||
getString(descriptionId)
|
||||
} ?: ""
|
||||
|
||||
if (cryptoPrompt.isBiometricOperation) {
|
||||
mBiometricPrompt?.authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(promptTitle)
|
||||
if (promptDescription.isNotEmpty())
|
||||
setDescription(promptDescription)
|
||||
setConfirmationRequired(false)
|
||||
if (isDeviceCredentialBiometricOperation(context)) {
|
||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||
} else {
|
||||
setNegativeButtonText(getString(android.R.string.cancel))
|
||||
}
|
||||
}.build(),
|
||||
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
||||
} else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||
context?.let { context ->
|
||||
@Suppress("DEPRECATION")
|
||||
mDeviceCredentialResultLauncher?.launch(
|
||||
ContextCompat.getSystemService(
|
||||
context,
|
||||
KeyguardManager::class.java
|
||||
)?.createConfirmDeviceCredentialIntent(
|
||||
promptTitle,
|
||||
promptDescription
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open prompt", e)
|
||||
mDeviceUnlockViewModel.setException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotAvailableMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(false)
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openBiometricSetting() {
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||
try {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||
@Suppress("DEPRECATION") context
|
||||
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
|
||||
}
|
||||
else -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSecurityUpdateRequiredMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||
openBiometricSetting()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotConfiguredMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||
openBiometricSetting()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setKeyManagerNotAvailableMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||
openBiometricSetting()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setWaitCredentialMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unavailable)
|
||||
context?.let { context ->
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||
mDeviceUnlockViewModel.setException(SecurityException(
|
||||
context.getString(R.string.credential_before_click_advanced_unlock_button)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setStoreCredentialMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||
context?.let { context ->
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view ->
|
||||
mDeviceUnlockViewModel.showPrompt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setExtractCredentialMode() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock)
|
||||
context?.let { context ->
|
||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view ->
|
||||
mDeviceUnlockViewModel.showPrompt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
mDeviceUnlockViewModel.deleteEncryptedDatabaseKey()
|
||||
}
|
||||
|
||||
private fun showViews(show: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (show) {
|
||||
if (mDeviceUnlockView?.visibility != View.VISIBLE)
|
||||
mDeviceUnlockView?.showByFading()
|
||||
}
|
||||
else {
|
||||
if (mDeviceUnlockView?.visibility == View.VISIBLE)
|
||||
mDeviceUnlockView?.hideByFading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mDeviceUnlockView?.setTitle(textId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||
// Ignore negative button
|
||||
}
|
||||
else ->
|
||||
mDeviceUnlockViewModel.setException(SecurityException(errString.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAuthenticationFailed() {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
mDeviceUnlockViewModel.setException(
|
||||
SecurityException(getString(R.string.advanced_unlock_not_recognized))
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
mDeviceUnlockView = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mDeviceUnlockViewModel.disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DeviceUnlockFragment::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class DeviceUnlockManager(private var appContext: Context) {
|
||||
|
||||
private var keyStore: KeyStore? = null
|
||||
private var keyGenerator: KeyGenerator? = null
|
||||
private var cipher: Cipher? = null
|
||||
|
||||
private var biometricUnlockEnable = isBiometricUnlockEnable(appContext)
|
||||
private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext)
|
||||
|
||||
init {
|
||||
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
|
||||
if (isDeviceSecure(appContext)) {
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
||||
this.keyGenerator = KeyGenerator.getInstance(
|
||||
ADVANCED_UNLOCK_KEY_ALGORITHM,
|
||||
ADVANCED_UNLOCK_KEYSTORE
|
||||
)
|
||||
this.cipher = Cipher.getInstance(
|
||||
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
||||
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
||||
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING
|
||||
)
|
||||
if (keyStore == null) {
|
||||
throw SecurityException("Unable to initialize the keystore")
|
||||
}
|
||||
if (keyGenerator == null) {
|
||||
throw SecurityException("Unable to initialize the key generator")
|
||||
}
|
||||
if (cipher == null) {
|
||||
throw SecurityException("Unable to initialize the cipher")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize the device unlock manager", e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
throw SecurityException("Device not secure enough")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||
try {
|
||||
// Create new key if needed
|
||||
keyStore?.let { keyStore ->
|
||||
keyStore.load(null)
|
||||
try {
|
||||
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator?.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
// To store in the security chip
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& appContext.packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||
throw e
|
||||
}
|
||||
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||
throw e
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized fun initEncryptData(
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
initEncryptData(true, actionIfCypherInit)
|
||||
}
|
||||
|
||||
@Synchronized private fun initEncryptData(
|
||||
firstLaunch: Boolean,
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
try {
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
actionIfCypherInit.invoke(
|
||||
DeviceUnlockCryptoPrompt(
|
||||
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
|
||||
cipher = cipher,
|
||||
titleId = R.string.advanced_unlock_prompt_store_credential_title,
|
||||
descriptionId = R.string.advanced_unlock_prompt_store_credential_message,
|
||||
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||
deviceCredentialUnlockEnable
|
||||
),
|
||||
isBiometricOperation = isBiometricOperation(
|
||||
biometricUnlockEnable, deviceCredentialUnlockEnable
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
throw unrecoverableKeyException
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(appContext)
|
||||
initEncryptData(false, actionIfCypherInit)
|
||||
} else {
|
||||
throw invalidKeyException
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun encryptData(
|
||||
value: ByteArray,
|
||||
cipher: Cipher?,
|
||||
handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit
|
||||
) {
|
||||
try {
|
||||
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
||||
// passes updated iv spec on to callback so this can be stored for decryption
|
||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||
handleEncryptedResult.invoke(encrypted, spec.iv)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to encrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun initDecryptData(
|
||||
ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
initDecryptData(ivSpecValue, true, actionIfCypherInit)
|
||||
}
|
||||
|
||||
@Synchronized private fun initDecryptData(
|
||||
ivSpecValue: ByteArray,
|
||||
firstLaunch: Boolean = true,
|
||||
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||
) {
|
||||
try {
|
||||
// important to restore spec here that was used for decryption
|
||||
val spec = IvParameterSpec(ivSpecValue)
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.let { cipher ->
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
actionIfCypherInit.invoke(
|
||||
DeviceUnlockCryptoPrompt(
|
||||
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
|
||||
cipher = cipher,
|
||||
titleId = R.string.advanced_unlock_prompt_extract_credential_title,
|
||||
descriptionId = null,
|
||||
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||
deviceCredentialUnlockEnable
|
||||
),
|
||||
isBiometricOperation = isBiometricOperation(
|
||||
biometricUnlockEnable, deviceCredentialUnlockEnable
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteKeystoreKey()
|
||||
initDecryptData(ivSpecValue, false, actionIfCypherInit)
|
||||
} else {
|
||||
throw unrecoverableKeyException
|
||||
}
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(appContext)
|
||||
initDecryptData(ivSpecValue, false, actionIfCypherInit)
|
||||
} else {
|
||||
throw invalidKeyException
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun decryptData(
|
||||
encryptedValue: ByteArray,
|
||||
cipher: Cipher?,
|
||||
handleDecryptedResult: (decryptedValue: ByteArray) -> Unit
|
||||
) {
|
||||
try {
|
||||
// actual decryption here
|
||||
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||
handleDecryptedResult.invoke(decrypted)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized fun deleteKeystoreKey() {
|
||||
try {
|
||||
keyStore?.load(null)
|
||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DeviceUnlockManager::class.java.name
|
||||
|
||||
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_STRONG
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_WEAK
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDeviceSecure(context: Context): Boolean {
|
||||
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
?.isDeviceSecure ?: false
|
||||
}
|
||||
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
}
|
||||
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
fun deleteEntryKeyInKeystoreForBiometric(
|
||||
appContext: Context
|
||||
) {
|
||||
DeviceUnlockManager(appContext).apply {
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) {
|
||||
try {
|
||||
deleteEntryKeyInKeystoreForBiometric(appContext)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(appContext,
|
||||
deviceUnlockError(e, appContext),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
} finally {
|
||||
CipherDatabaseAction.getInstance(appContext).deleteAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deviceUnlockError(error: Exception, context: Context): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& (error is UnrecoverableKeyException
|
||||
|| error is KeyPermanentlyInvalidatedException)) {
|
||||
context.getString(R.string.advanced_unlock_invalid_key)
|
||||
} else
|
||||
error.cause?.localizedMessage
|
||||
?: error.localizedMessage
|
||||
?: error.toString()
|
||||
}
|
||||
|
||||
fun isBiometricUnlockEnable(appContext: Context) =
|
||||
PreferencesUtil.isBiometricUnlockEnable(appContext)
|
||||
|
||||
fun isDeviceCredentialUnlockEnable(appContext: Context) =
|
||||
PreferencesUtil.isDeviceCredentialUnlockEnable(appContext)
|
||||
|
||||
private fun isBiometricOperation(
|
||||
biometricUnlockEnable: Boolean,
|
||||
deviceCredentialUnlockEnable: Boolean
|
||||
): Boolean {
|
||||
return biometricUnlockEnable
|
||||
|| isDeviceCredentialBiometricOperation(deviceCredentialUnlockEnable)
|
||||
}
|
||||
|
||||
// Since Android 30, device credential is also a biometric operation
|
||||
private fun isDeviceCredentialOperation(
|
||||
deviceCredentialUnlockEnable: Boolean
|
||||
): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
private fun isDeviceCredentialBiometricOperation(
|
||||
deviceCredentialUnlockEnable: Boolean
|
||||
): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable
|
||||
}
|
||||
|
||||
fun isDeviceCredentialBiometricOperation(context: Context?): Boolean {
|
||||
if (context == null) {
|
||||
return false
|
||||
}
|
||||
return isDeviceCredentialBiometricOperation(
|
||||
isDeviceCredentialUnlockEnable(context)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
enum class DeviceUnlockMode {
|
||||
BIOMETRIC_UNAVAILABLE,
|
||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
||||
KEY_MANAGER_UNAVAILABLE,
|
||||
WAIT_CREDENTIAL,
|
||||
STORE_CREDENTIAL,
|
||||
EXTRACT_CREDENTIAL
|
||||
}
|
||||
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
@@ -251,7 +251,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
||||
|
||||
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.biometricUnlockSupported(activity)
|
||||
DeviceUnlockManager.biometricUnlockSupported(activity)
|
||||
} else false
|
||||
biometricUnlockEnablePreference?.apply {
|
||||
// False if under Marshmallow
|
||||
@@ -296,7 +296,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||
DeviceUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||
} else false
|
||||
deviceCredentialUnlockEnablePreference?.apply {
|
||||
// Biometric unlock already checked
|
||||
@@ -395,7 +395,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
validate?.invoke()
|
||||
warningAlertDialog?.setOnDismissListener(null)
|
||||
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||
DeviceUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||
|
||||
@@ -29,7 +29,7 @@ import androidx.preference.PreferenceManager
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
@@ -512,7 +512,7 @@ object PreferencesUtil {
|
||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
||||
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.biometricUnlockSupported(context)
|
||||
DeviceUnlockManager.biometricUnlockSupported(context)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
|
||||
@@ -25,15 +25,14 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
class DeviceUnlockView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: LinearLayout(context, attrs, defStyle) {
|
||||
|
||||
private var biometricButtonView: Button? = null
|
||||
@@ -45,7 +44,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
biometricButtonView = findViewById(R.id.biometric_button)
|
||||
}
|
||||
|
||||
fun setIconViewClickListener(listener: OnClickListener?) {
|
||||
fun setDeviceUnlockButtonViewClickListener(listener: OnClickListener?) {
|
||||
biometricButtonView?.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
@@ -60,14 +59,4 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
fun setTitle(@StringRes textId: Int) {
|
||||
title = context.getString(textId)
|
||||
}
|
||||
|
||||
fun setMessage(text: CharSequence) {
|
||||
if (text.isNotEmpty())
|
||||
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun setMessage(@StringRes textId: Int) {
|
||||
Toast.makeText(context, textId, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class AdvancedUnlockViewModel : ViewModel() {
|
||||
|
||||
var allowAutoOpenBiometricPrompt : Boolean = true
|
||||
var deviceCredentialAuthSucceeded: Boolean? = null
|
||||
|
||||
private val _uiState = MutableStateFlow(DeviceUnlockState())
|
||||
val uiState: StateFlow<DeviceUnlockState> = _uiState
|
||||
|
||||
fun checkUnlockAvailability(conditionToStoreCredentialVerified: Boolean? = null) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
onUnlockAvailabilityCheckRequested = true,
|
||||
isConditionToStoreCredentialVerified = conditionToStoreCredentialVerified
|
||||
?: _uiState.value.isConditionToStoreCredentialVerified
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCheckUnlockAvailability() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
onUnlockAvailabilityCheckRequested = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun databaseFileLoaded(databaseUri: Uri?) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
databaseFileLoaded = databaseUri
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeDatabaseFileLoaded() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
databaseFileLoaded = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveCredentialForEncryption() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
isCredentialRequired = true,
|
||||
credential = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun provideCredentialForEncryption(credential: ByteArray) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
isCredentialRequired = false,
|
||||
credential = credential
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialForEncryption() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
isCredentialRequired = false,
|
||||
credential = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherEncryptDatabase = cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialEncrypted() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherEncryptDatabase = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherDecryptDatabase = cipherDecryptDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialDecrypted() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherDecryptDatabase = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceUnlockState(
|
||||
val initAdvancedUnlockMode: Boolean = false,
|
||||
val databaseFileLoaded: Uri? = null,
|
||||
val isCredentialRequired: Boolean = false,
|
||||
val credential: ByteArray? = null,
|
||||
val isConditionToStoreCredentialVerified: Boolean = false,
|
||||
val onUnlockAvailabilityCheckRequested: Boolean = false,
|
||||
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
|
||||
val cipherDecryptDatabase: CipherDecryptDatabase? = null
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DeviceUnlockState
|
||||
|
||||
if (initAdvancedUnlockMode != other.initAdvancedUnlockMode) return false
|
||||
if (isCredentialRequired != other.isCredentialRequired) return false
|
||||
if (isConditionToStoreCredentialVerified != other.isConditionToStoreCredentialVerified) return false
|
||||
if (onUnlockAvailabilityCheckRequested != other.onUnlockAvailabilityCheckRequested) return false
|
||||
if (databaseFileLoaded != other.databaseFileLoaded) return false
|
||||
if (!credential.contentEquals(other.credential)) return false
|
||||
if (cipherEncryptDatabase != other.cipherEncryptDatabase) return false
|
||||
if (cipherDecryptDatabase != other.cipherDecryptDatabase) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = initAdvancedUnlockMode.hashCode()
|
||||
result = 31 * result + isCredentialRequired.hashCode()
|
||||
result = 31 * result + isConditionToStoreCredentialVerified.hashCode()
|
||||
result = 31 * result + onUnlockAvailabilityCheckRequested.hashCode()
|
||||
result = 31 * result + (databaseFileLoaded?.hashCode() ?: 0)
|
||||
result = 31 * result + (credential?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (cipherEncryptDatabase?.hashCode() ?: 0)
|
||||
result = 31 * result + (cipherDecryptDatabase?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPrompt
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPromptType
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockMode
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.crypto.Cipher
|
||||
|
||||
class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) {
|
||||
|
||||
var allowAutoOpenBiometricPrompt : Boolean = true
|
||||
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||
|
||||
private var isConditionToStoreCredentialVerified: Boolean = false
|
||||
|
||||
private var deviceUnlockManager: DeviceUnlockManager? = null
|
||||
private var databaseUri: Uri? = null
|
||||
|
||||
private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
||||
var cryptoPrompt: DeviceUnlockCryptoPrompt? = null
|
||||
|
||||
// TODO Retrieve credential storage from app database
|
||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||
|
||||
val cipherDatabaseAction = CipherDatabaseAction.getInstance(getApplication())
|
||||
|
||||
private val _uiState = MutableStateFlow(DeviceUnlockState())
|
||||
val uiState: StateFlow<DeviceUnlockState> = _uiState
|
||||
|
||||
fun checkConditionToStoreCredential(condition: Boolean, databaseFileUri: Uri?) {
|
||||
isConditionToStoreCredentialVerified = condition
|
||||
checkUnlockAvailability(databaseFileUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability by verifying device settings and database mode
|
||||
*/
|
||||
fun checkUnlockAvailability() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipherDatabase ->
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(getApplication())) {
|
||||
// biometric not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(getApplication())
|
||||
if (!PreferencesUtil.isAdvancedUnlockEnable(getApplication())
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||
changeMode(DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||
} else {
|
||||
// biometric is available but not configured, show icon but in disabled state with some information
|
||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
} else {
|
||||
selectMode(containsCipherDatabase)
|
||||
}
|
||||
}
|
||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(getApplication())) {
|
||||
if (DeviceUnlockManager.isDeviceSecure(getApplication())) {
|
||||
selectMode(containsCipherDatabase)
|
||||
} else {
|
||||
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability and change the current mode depending of device's state
|
||||
*/
|
||||
fun checkUnlockAvailability(databaseFileUri: Uri?) {
|
||||
databaseUri = databaseFileUri
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun selectMode(containsCipherDatabase: Boolean) {
|
||||
try {
|
||||
if (isConditionToStoreCredentialVerified) {
|
||||
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
||||
// listen for encryption
|
||||
changeMode(DeviceUnlockMode.STORE_CREDENTIAL)
|
||||
initEncryptData()
|
||||
} else if (containsCipherDatabase) {
|
||||
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
||||
// biometric available but no stored password found yet for this DB
|
||||
// listen for decryption
|
||||
changeMode(DeviceUnlockMode.EXTRACT_CREDENTIAL)
|
||||
initDecryptData()
|
||||
} else {
|
||||
// wait for typing
|
||||
changeMode(DeviceUnlockMode.WAIT_CREDENTIAL)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
changeMode(DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE)
|
||||
setException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun connect(databaseUri: Uri) {
|
||||
this.databaseUri = databaseUri
|
||||
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||
override fun onCipherDatabaseCleared() {
|
||||
closeBiometricPrompt()
|
||||
checkUnlockAvailability(databaseUri)
|
||||
}
|
||||
}
|
||||
cipherDatabaseAction.apply {
|
||||
reloadPreferences()
|
||||
cipherDatabaseListener?.let {
|
||||
registerDatabaseListener(it)
|
||||
}
|
||||
}
|
||||
checkUnlockAvailability(databaseUri)
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
this.databaseUri = null
|
||||
cipherDatabaseListener?.let {
|
||||
cipherDatabaseAction.unregisterDatabaseListener(it)
|
||||
}
|
||||
reset()
|
||||
}
|
||||
|
||||
fun databaseFileLoaded(databaseUri: Uri?) {
|
||||
// To get device credential unlock result, only if same database uri
|
||||
if (databaseUri != null
|
||||
&& PreferencesUtil.isAdvancedUnlockEnable(getApplication())) {
|
||||
if (databaseUri != this.databaseUri) {
|
||||
connect(databaseUri)
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun onAuthenticationSucceeded(
|
||||
activityResult: ActivityResult
|
||||
) {
|
||||
cryptoPrompt?.let { prompt ->
|
||||
when (prompt.type) {
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
||||
retrieveCredentialForEncryption( prompt.cipher)
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
||||
decryptCredential( prompt.cipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
cryptoPrompt?.type?.let { type ->
|
||||
when (type) {
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
||||
retrieveCredentialForEncryption(result.cryptoObject?.cipher)
|
||||
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
||||
decryptCredential(result.cryptoObject?.cipher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveCredentialForEncryption(cipher: Cipher?) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
credentialRequiredCipher = cipher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun encryptCredential(
|
||||
credential: ByteArray,
|
||||
cipher: Cipher?
|
||||
) {
|
||||
try {
|
||||
deviceUnlockManager?.encryptData(
|
||||
value = credential,
|
||||
cipher = cipher,
|
||||
handleEncryptedResult = { encryptedValue, ivSpec ->
|
||||
databaseUri?.let { databaseUri ->
|
||||
onCredentialEncrypted(
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.encryptedValue = encryptedValue
|
||||
this.specParameters = ivSpec
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
setException(e)
|
||||
} finally {
|
||||
// Reinit credential storage request
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
credentialRequiredCipher = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun decryptCredential(cipher: Cipher?) {
|
||||
// retrieve the encrypted value from preferences
|
||||
databaseUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||
cipherDatabase?.encryptedValue?.let { encryptedCredential ->
|
||||
try {
|
||||
deviceUnlockManager?.decryptData(
|
||||
encryptedValue = encryptedCredential,
|
||||
cipher = cipher,
|
||||
handleDecryptedResult = { decryptedValue ->
|
||||
// Load database directly with password retrieve
|
||||
onCredentialDecrypted(
|
||||
CipherDecryptDatabase().apply {
|
||||
this.databaseUri = databaseUri
|
||||
this.credentialStorage = credentialDatabaseStorage
|
||||
this.decryptedValue = decryptedValue
|
||||
}
|
||||
)
|
||||
cipherDatabaseAction.resetCipherParameters(databaseUri)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
setException(e)
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: run {
|
||||
setException(UnknownDatabaseLocationException())
|
||||
}
|
||||
}
|
||||
|
||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherEncryptDatabase = cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialEncrypted() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherEncryptDatabase = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherDecryptDatabase = cipherDecryptDatabase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeCredentialDecrypted() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cipherDecryptDatabase = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPromptRequested(
|
||||
cryptoPrompt: DeviceUnlockCryptoPrompt,
|
||||
autoOpen: Boolean = false
|
||||
) {
|
||||
this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt
|
||||
if (autoOpen && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication()))
|
||||
showPrompt()
|
||||
}
|
||||
|
||||
fun showPrompt() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.SHOW
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun promptShown() {
|
||||
allowAutoOpenBiometricPrompt = false
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.IDLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setException(value: Exception?) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
exception = value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun exceptionShown() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
exception = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initEncryptData() {
|
||||
try {
|
||||
deviceUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||
onPromptRequested(cryptoPrompt)
|
||||
} ?: setException(Exception("AdvancedUnlockManager not initialized"))
|
||||
} catch (e: Exception) {
|
||||
setException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initDecryptData() {
|
||||
databaseUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||
cipherDatabase?.let {
|
||||
try {
|
||||
deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt ->
|
||||
onPromptRequested(cryptoPrompt, autoOpen = allowAutoOpenBiometricPrompt)
|
||||
} ?: setException(Exception("AdvancedUnlockManager not initialized"))
|
||||
} catch (e: Exception) {
|
||||
setException(e)
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: setException(UnknownDatabaseLocationException())
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun changeMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||
this.deviceUnlockMode = deviceUnlockMode
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
newDeviceUnlockMode = deviceUnlockMode,
|
||||
allowAdvancedUnlockMenu = containsCipher
|
||||
&& deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
||||
&& deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
closeBiometricPrompt()
|
||||
databaseUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||
checkUnlockAvailability(databaseUri)
|
||||
}
|
||||
} ?: checkUnlockAvailability(null)
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
allowAdvancedUnlockMenu = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeBiometricPrompt() {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.CLOSE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun biometricPromptClosed() {
|
||||
cryptoPrompt = null
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
cryptoPromptState = DeviceUnlockPromptMode.IDLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deviceUnlockManager = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeviceUnlockPromptMode {
|
||||
IDLE, SHOW, CLOSE
|
||||
}
|
||||
|
||||
data class DeviceUnlockState(
|
||||
val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE,
|
||||
val allowAdvancedUnlockMenu: Boolean = false,
|
||||
val credentialRequiredCipher: Cipher? = null,
|
||||
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
|
||||
val cipherDecryptDatabase: CipherDecryptDatabase? = null,
|
||||
val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE,
|
||||
val autoOpenPrompt: Boolean = false,
|
||||
val exception: Exception? = null
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
<com.kunzisoft.keepass.view.DeviceUnlockView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/advanced_unlock_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -648,7 +648,6 @@
|
||||
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
||||
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
||||
<string name="advanced_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
||||
<string name="advanced_unlock_scanning_error">خطأ في فتح الجهاز: %1$s</string>
|
||||
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
||||
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
<string name="keystore_not_accessible">Açar ehtiyyatı düzgün formada başladılmadı.</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Cihaz kilidini açma linki</string>
|
||||
<string name="database_history">Tarixçə</string>
|
||||
<string name="advanced_unlock_scanning_error">Cihaz kilidini açma xətası: %1$s</string>
|
||||
<string name="warning_database_info_reloaded">Məlumat bazasını yenidən yükləmək lokal olaraq modifikasiya olunmuş faylları siləcəkdir.</string>
|
||||
<string name="warning_database_revoked">Fayla giriş fayl meneceri tərəfindən ləğv edildi, məlumat bazasını bağlayın və onu olduğu yerdən yenidən açın.</string>
|
||||
<string name="warning_exact_alarm">Siz tətəbiqin zəngli saatdan istifadə etməsinə icazə verməmisiniz. Nəticədə, taymer tələb edən funksiyalar dəqiq bir zamanda işləməyəckdir.</string>
|
||||
|
||||
@@ -536,7 +536,6 @@
|
||||
<string name="recycle_bin_title">Използване на кошчето</string>
|
||||
<string name="recycle_bin_summary">Премества групите и записите в групата „Кошче“ вместо да ги премахва директно</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Отключване с устройството</string>
|
||||
<string name="advanced_unlock_scanning_error">Грешка при отключване на устройството: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Не може да бъде разпознато кога устройството е отключено</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Заявката за отключване не може да бъде подготвена.</string>
|
||||
<string name="password_size_summary">Подразбирана дължина на създаваните пароли</string>
|
||||
|
||||
@@ -613,7 +613,6 @@
|
||||
<string name="configure">Configura</string>
|
||||
<string name="biometric_security_update_required">Cal actualitzar la seguretat biomètrica.</string>
|
||||
<string name="unlock_and_link_biometric">Enllaç de desbloqueig del dispositiu</string>
|
||||
<string name="advanced_unlock_scanning_error">Error en desbloquejar el dispositiu: %1$s</string>
|
||||
<string name="unavailable">No disponible</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">No s\'ha pogut inicialitzar l\'indicador de desbloqueig del dispositiu.</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Escriviu la contrasenya i, a continuació, feu clic en aquest botó.</string>
|
||||
|
||||
@@ -499,7 +499,6 @@
|
||||
<string name="device_credential">Heslo zařízení</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Zadejte heslo a pak klepněte na toto tlačítko.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Nepodařilo se inicializovat nabídku pro odemykání zařízení.</string>
|
||||
<string name="advanced_unlock_scanning_error">Chyba při odemykání zařízení: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Otisk pro odemykání zařízení nebyl rozpoznán</string>
|
||||
<string name="advanced_unlock_invalid_key">Nepodařilo se načíst klíč odemykání zařízení. Odstraňte ho a opakujte proces rozpoznání odemknutí.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Načíst údaj z databáze pomocí dat odemykání zařízení</string>
|
||||
|
||||
@@ -507,7 +507,6 @@
|
||||
<string name="content">Indhold</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Indtast adgangskoden, og klik derefter på denne knap.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Kunne ikke initialisere oplåsningsprompt.</string>
|
||||
<string name="advanced_unlock_scanning_error">Fejl ved oplåsning: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Kunne ikke genkende aftryk til oplåsning</string>
|
||||
<string name="advanced_unlock_invalid_key">Oplåsningsnøgle kan ikke læses. Slet den og gentag proceduren for genkendelse af oplåsning.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Enhedsoplåsningsgenkendelse</string>
|
||||
|
||||
@@ -529,7 +529,6 @@
|
||||
<string name="device_credential">Geräteanmeldedaten</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Passwort eingeben und dann diese Taste drücken.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Geräteentsperrungsabfrage konnte nicht gestartet werden.</string>
|
||||
<string name="advanced_unlock_scanning_error">Fehler bei Geräteentsperrung: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Fingerabdruck für Geräteentsperrung wurde nicht erkannt</string>
|
||||
<string name="advanced_unlock_invalid_key">Der Geräteentsperrschlüssel ist nicht lesbar. Bitte diesen löschen und den Vorgang zur Entsperr-Erkennung wiederholen.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Datenbankanmeldedaten aus Geräteentsperrdaten gewinnen</string>
|
||||
|
||||
@@ -503,7 +503,6 @@
|
||||
<string name="credential_before_click_advanced_unlock_button">Πληκτρολογήστε τον κωδικό πρόσβασης, και στη συνέχεια κάντε κλικ αυτό το κουμπί.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Δεν είναι δυνατή η προετοιμασία της προτροπής ξεκλειδώματος συσκευής.</string>
|
||||
<string name="advanced_unlock_not_recognized">Δεν ήταν δυνατή η αναγνώριση αποτυπώματος ξεκλειδώματος συσκευής</string>
|
||||
<string name="advanced_unlock_scanning_error">Σφάλμα ξεκλειδώματος συσκευής: %1$s</string>
|
||||
<string name="advanced_unlock_invalid_key">Δεν είναι δυνατή η ανάγνωση του κλειδιού ξεκλειδώματος της συσκευής. Διαγράψτε το και επαναλάβετε τη διαδικασία αναγνώρισης ξεκλειδώματος.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Εξαγωγή διαπιστευτηρίων βάσης δεδομένων με δεδομένα ξεκλειδώματος συσκευής</string>
|
||||
<string name="education_advanced_unlock_summary">Συνδέστε τον κωδικό πρόσβασής σας με το σαρωμένο βιομετρικό ή τα διαπιστευτήρια της συσκευής σας για να ξεκλειδώσετε γρήγορα τη βάση δεδομένων σας.</string>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<string name="colorize_password_summary">Colourise password characters by type</string>
|
||||
<string name="content_description_entry_background_color">Entry background colour</string>
|
||||
<string name="invalid_db_sig">Could not recognise the database format.</string>
|
||||
<string name="advanced_unlock_not_recognized">Could not recognise advanced unlock print</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Unable to initialise advanced unlock prompt.</string>
|
||||
<string name="advanced_unlock_not_recognized">Could not recognise device unlock print</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Unable to initialise device unlock prompt.</string>
|
||||
<string name="download_initialization">Initialising…</string>
|
||||
<string name="download_finalization">Finalising…</string>
|
||||
<string name="download_canceled">Cancelled!</string>
|
||||
|
||||
@@ -444,7 +444,6 @@
|
||||
<string name="device_credential">Credenciales del dispositivo</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Teclee la contraseña y luego pulse sobre este botón.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">No se puede inicializar el aviso de desbloqueo avanzado.</string>
|
||||
<string name="advanced_unlock_scanning_error">Error de desbloqueo del dispositivo: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">No se ha podido reconocer la impresión de desbloqueo avanzado</string>
|
||||
<string name="advanced_unlock_invalid_key">No se puede leer la clave de desbloqueo del dispositivo. Por favor, bórrala y repite el procedimiento de reconocimiento del desbloqueo.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extraer la credencial de la base de datos con los datos de desbloqueo del dispositivo</string>
|
||||
|
||||
@@ -440,7 +440,6 @@
|
||||
<string name="merge_success">Mestimine õnnestus</string>
|
||||
<string name="biometric_security_update_required">Vajalik on biomeetrilise turvalisuse uuendus.</string>
|
||||
<string name="encrypted_value_stored">Krüptitud salasõna on salvestatud</string>
|
||||
<string name="advanced_unlock_scanning_error">Viga seadme lukustuse eemaldamisel: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Ei õnnestunud tuvastada lukustuse eemaldamiseks vajalikku tunnust</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Seadme lukustuse eemaldamise päringu käivitamine ei õnnestu.</string>
|
||||
<string name="biometric">Biomeetriline</string>
|
||||
|
||||
@@ -489,7 +489,6 @@
|
||||
<string name="advanced_unlock_prompt_store_credential_message">Zure kutxa gotorraren pasahitz-nagusia gogoratu behar duzu naiz eta desblokeo aurreratuko ezagutzea erabili arren.</string>
|
||||
<string name="encrypted_value_stored">Zifratutako pasahitza gordeta</string>
|
||||
<string name="unavailable">Datu-base honek ez du biltegiratuta kredentzialik.</string>
|
||||
<string name="advanced_unlock_scanning_error">Gailuaren desblokeatze errorea: %1$s</string>
|
||||
<string name="menu_appearance_settings">Itxura</string>
|
||||
<string name="autofill_sign_in_prompt">KeePassDXekin erregistratu</string>
|
||||
<string name="autofill_explanation_summary">Gaitu betetze automatikoa beste aplikazioetako formularioak errez betetzeko</string>
|
||||
|
||||
@@ -509,7 +509,6 @@
|
||||
<string name="device_credential">Identifiant de l\'appareil</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Tapez le mot de passe, puis cliquez sur ce bouton.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Impossible d\'initialiser l\'invite de déverrouillage avancé.</string>
|
||||
<string name="advanced_unlock_scanning_error">Erreur de déverrouillage avancé : %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Impossible de reconnaître l\'empreinte de déverrouillage de l\'appareil</string>
|
||||
<string name="advanced_unlock_invalid_key">Impossible de lire la clé de déverrouillage de l\'appareil. Veuillez la supprimer et répéter la procédure de reconnaissance de déverrouillage.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extraire les identifiants de la base de données avec des données de déverrouillage de l\'appareil</string>
|
||||
|
||||
@@ -425,7 +425,6 @@
|
||||
<string name="advanced_unlock_invalid_key">Non foi posíbel ler a clave de desbloqueo avanzado. Por favor, bórrea e repita o procedemento de recoñecemento do desbloqueo.</string>
|
||||
<string name="encrypted_value_stored">Contrasinal cifrado almacenado</string>
|
||||
<string name="advanced_unlock_not_recognized">Non foi posíbel recoñecer a pegada do desbloqueo avanzado</string>
|
||||
<string name="advanced_unlock_scanning_error">Erro de desbloqueo avanzado: %1$s</string>
|
||||
<string name="autofill_service_name">Servizo de autocompletado do KeePassDX</string>
|
||||
<string name="database_history">Historial</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">Aínda precisa lembrar a súa credencial principal se usar o recoñecemento de desbloqueo avanzado.</string>
|
||||
|
||||
@@ -490,7 +490,6 @@
|
||||
<string name="menu_keystore_remove_key">Izbriši ključ za otključavanje uređaja</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Poveznica za otključavanje uređaja</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Nije moguće pokrenuti prozor za otključavanje uređaja.</string>
|
||||
<string name="advanced_unlock_scanning_error">Greška otključavanja uređaja: %1$s</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Izdvoji podatake za prijavu na bazu podataka pomoću podataka za otključavanje uređaja</string>
|
||||
<string name="advanced_unlock_not_recognized">Nije bilo moguće prepoznati ispis za otključavanje uređaja</string>
|
||||
<string name="advanced_unlock_invalid_key">Nije moguće pročitati ključ za otključavanje uređaja. Izbriši ga i ponovi postupak prepoznavanja otključavanja.</string>
|
||||
|
||||
@@ -537,7 +537,6 @@
|
||||
<string name="credential_before_click_advanced_unlock_button">Írja be a jelszót, majd kattintson erre a gombra.</string>
|
||||
<string name="temp_advanced_unlock_enable_title">Ideiglenes eszközfeloldás</string>
|
||||
<string name="permission">Engedély</string>
|
||||
<string name="advanced_unlock_scanning_error">Eszközfeloldási hiba: %1$s</string>
|
||||
<string name="content">Tartalom</string>
|
||||
<string name="advanced_unlock_tap_delete">Koppintson az eszközfeloldási kulcsok törléséhez</string>
|
||||
<string name="device_credential_unlock_enable_title">Eszköz hitelesítő adataival történő feloldás</string>
|
||||
|
||||
@@ -582,7 +582,6 @@
|
||||
<string name="upper_case">HURUF BESAR</string>
|
||||
<string name="title_case">Huruf Judul</string>
|
||||
<string name="character_count">Jumlah karakter: %1$d</string>
|
||||
<string name="advanced_unlock_scanning_error">Terjadi kesalahan buka kunci perangkat: %1$s</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Tidak dapat menginisialisasi perintah buka kunci perangkat.</string>
|
||||
<string name="monospace_font_fields_enable_title">Bidang tipe huruf</string>
|
||||
<string name="keyboard_save_search_info_title">Simpan info terbagi</string>
|
||||
|
||||
@@ -515,7 +515,6 @@
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Collegamento allo sblocco con dispositivo</string>
|
||||
<string name="device_credential">Credenziali del dispositivo</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Inserisci la password, poi clicca questo pulsante.</string>
|
||||
<string name="advanced_unlock_scanning_error">Errore sblocco con dispositivo: %1$s</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Riconoscimento sblocco con dispositivo</string>
|
||||
<string name="menu_keystore_remove_key">Elimina chiave di sblocco del dispositivo</string>
|
||||
<string name="error_rebuild_list">Non è possibile ricostruire la lista correttamente.</string>
|
||||
|
||||
@@ -539,7 +539,6 @@
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">חלץ אישור מסד נתונים עם נתוני ביטול נעילת מכשיר</string>
|
||||
<string name="advanced_unlock_invalid_key">לא ניתן לקרוא את מפתח ביטול נעילת המכשיר. נא למחוק אותו ולחזור על התהליך לזיהוי ביטול נעילה.</string>
|
||||
<string name="advanced_unlock_not_recognized">לא היה ניתן לזהות טביעת ביטול נעילת מכשיר</string>
|
||||
<string name="advanced_unlock_scanning_error">שגיאת ביטול נעילת מכשיר: %1$s</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">הקלד את הסיסמה, ואז לחץ על הכפתור הזה.</string>
|
||||
<string name="lock_database_show_button_title">הצג כפתור נעילה</string>
|
||||
<string name="lock_database_show_button_summary">הצג את כפתור הנעילה בממשק המשתמש</string>
|
||||
|
||||
@@ -491,7 +491,6 @@
|
||||
<string name="device_credential">デバイス認証情報</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">パスワードを入力し、このボタンをタップします。</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">デバイスのロック解除プロンプトを初期化できません。</string>
|
||||
<string name="advanced_unlock_scanning_error">デバイスのロック解除エラー: %1$s</string>
|
||||
<string name="advanced_unlock_invalid_key">デバイスのロック解除キーを読み取ることができません。削除して、ロック解除認識手順を繰り返してください。</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">デバイスのロック解除データを使用してデータベースの資格情報を抽出する</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">デバイスのロック解除認識</string>
|
||||
|
||||
@@ -395,7 +395,6 @@
|
||||
<string name="configure">Konfigūruoti</string>
|
||||
<string name="biometric_security_update_required">Reikalingas biometrinių duomenų saugumo atnaujinimas.</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">Jei naudojate įrenginio atrakinimo atpažinimą, vis tiek turite prisiminti pagrindinį saugyklos raktą</string>
|
||||
<string name="advanced_unlock_scanning_error">Įrenginio atrakinimo klaida: %1$s</string>
|
||||
<string name="database_history">Istorija</string>
|
||||
<string name="properties">Nustatymai</string>
|
||||
<string name="menu_appearance_settings_summary">Temos, spalvos, atributai</string>
|
||||
|
||||
@@ -405,7 +405,6 @@
|
||||
<string name="hide_broken_locations_summary">Skjul ødelagte lenker i listen over nylige databaser</string>
|
||||
<string name="hide_broken_locations_title">Skjul ødelagte databaselenker</string>
|
||||
<string name="autofill_ask_to_save_data_title">Spør om lagring av data</string>
|
||||
<string name="advanced_unlock_scanning_error">Feil ved opplåsing: %1$s</string>
|
||||
<string name="warning_empty_keyfile">Det anbefales ikke å legge til en tom nøkkelfil.</string>
|
||||
<string name="warning_sure_add_file">Legg til filen uansett\?</string>
|
||||
<string name="registration_mode">Registreringsmodus</string>
|
||||
|
||||
@@ -508,7 +508,6 @@
|
||||
<string name="device_credential">Apparaatreferentie</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Typ het wachtwoord en klik vervolgens op deze knop.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Kan apparaatontgrendeling niet initialiseren.</string>
|
||||
<string name="advanced_unlock_scanning_error">Fout bij apparaatontgrendeling: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Vingerafdruk niet herkent bij apparaatontgrendeling</string>
|
||||
<string name="advanced_unlock_invalid_key">Kan de sleutel voor apparaatontgrendeling niet lezen. Verwijder deze en herhaal de herkenningsprocedure voor het ontgrendelen.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Database uitpakken met gegevens voor apparaatontgrendeling</string>
|
||||
|
||||
@@ -511,7 +511,6 @@
|
||||
<string name="advanced_unlock_tap_delete">Stuknij, aby usunąć klucze odblokowywania urządzenia</string>
|
||||
<string name="content">Zawartość</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Rozpoznawanie odblokowania urządzenia</string>
|
||||
<string name="advanced_unlock_scanning_error">Błąd odblokowania urządzenia: %1$s</string>
|
||||
<string name="error_rebuild_list">Nie można poprawnie odbudować listy.</string>
|
||||
<string name="error_database_uri_null">Nie można pobrać identyfikatora URI bazy danych.</string>
|
||||
<string name="autofill_inline_suggestions_keyboard">Dodano sugestie autouzupełniania.</string>
|
||||
|
||||
@@ -512,7 +512,6 @@
|
||||
<string name="properties">Propriedades</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Digite a senha e clique neste botão.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Não foi possível inicializar o prompt de desbloqueio do dispositivo.</string>
|
||||
<string name="advanced_unlock_scanning_error">Erro de desbloqueio do dispositivo: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Não foi possível reconhecer a impressão de desbloqueio</string>
|
||||
<string name="advanced_unlock_invalid_key">Não é possível ler a chave de desbloqueio do dispositivo. Exclua-o e repita o procedimento de reconhecimento de desbloqueio.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extraia a credencial do banco de dados com os dados de desbloqueio do dispositivo</string>
|
||||
|
||||
@@ -468,7 +468,6 @@
|
||||
<string name="device_credential">Credencial do dispositivo</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Digite a palavra-passe e depois clique neste botão.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Não foi possível inicializar a solicitação de desbloqueio do dispositivo.</string>
|
||||
<string name="advanced_unlock_scanning_error">Erro de desbloqueio do dispositivo: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Não foi possível reconhecer a impressão de desbloqueio do dispositivo</string>
|
||||
<string name="advanced_unlock_invalid_key">Não é possível ler a chave de desbloqueio do dispositivo. Elimine-a e repita o procedimento de reconhecimento de desbloqueio.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extrair credencial da base de dados com dados de desbloqueio do dispositivo</string>
|
||||
|
||||
@@ -446,7 +446,6 @@
|
||||
<string name="accept">Aceitar</string>
|
||||
<string name="device_credential">Credencial do dispositivo</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Não foi possível inicializar a solicitação de desbloqueio do dispositivo.</string>
|
||||
<string name="advanced_unlock_scanning_error">Erro de desbloqueio do dispositivo: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Não foi possível reconhecer a impressão de desbloqueio do dispositivo</string>
|
||||
<string name="advanced_unlock_invalid_key">Não é possível ler a chave de desbloqueio do dispositivo. Elimine-a e repita o procedimento de reconhecimento de desbloqueio.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extrair credencial da base de dados com dados de desbloqueio do dispositivo</string>
|
||||
|
||||
@@ -597,7 +597,6 @@
|
||||
<string name="warning_copy_permission">Permisiunea de notificare este necesară pentru a utiliza funcția de notificare a clipboardului.</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Legătură la deblocarea dispozitivului</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Recunoașterea deblocării dispozitivului</string>
|
||||
<string name="advanced_unlock_scanning_error">Eroare de deblocare a dispozitivului: %1$s</string>
|
||||
<string name="unlock_and_link_biometric">Legătură de deblocare a dispozitivului</string>
|
||||
<string name="configure_biometric">Nu se înrolează nicio credențială biometrică sau de dispozitiv.</string>
|
||||
<string name="autofill_select_entry">Selectați intrarea…</string>
|
||||
|
||||
@@ -498,7 +498,6 @@
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Распознавание разблокировки устройства</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">При использовании разблокировки устройства вам всё равно необходимо помнить основные учётные данные.</string>
|
||||
<string name="advanced_unlock_delete_all_key_warning">Удалить все ключи шифрования, связанные с распознаванием разблокировки устройства\?</string>
|
||||
<string name="advanced_unlock_scanning_error">Ошибка разблокировки устройства: %1$s</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">Настройка разблокировки устройства</string>
|
||||
<string name="menu_keystore_remove_key">Удалить ключ разблокировки устройства</string>
|
||||
<string name="enter">Ввод</string>
|
||||
|
||||
@@ -476,7 +476,6 @@
|
||||
<string name="style_choose_title">Téma aplikácie</string>
|
||||
<string name="protection">Ochrana</string>
|
||||
<string name="configure_biometric">Nie sú zaregistrované žiadne biometrické údaje ani poverenia zariadenia.</string>
|
||||
<string name="advanced_unlock_scanning_error">Chyba odomykania zariadenia: %1$s</string>
|
||||
<string name="lock">Zamknúť</string>
|
||||
<string name="custom_fields">Vlastné polia</string>
|
||||
<string name="education_sort_summary">Vyberte spôsob triedenia záznamov a skupín.</string>
|
||||
|
||||
@@ -344,7 +344,6 @@
|
||||
<string name="show_entry_colors_title">Ngjyra zërash</string>
|
||||
<string name="hide_expired_entries_title">Fshihi zërat e skaduar</string>
|
||||
<string name="error_XML_malformed">XML e keqformuar.</string>
|
||||
<string name="advanced_unlock_scanning_error">Gabim shkyçjeje pajisjeje: %1$s</string>
|
||||
<string name="autofill_block">Blloko vetëplotësim</string>
|
||||
<string name="allow_no_password_summary">Lejon prekjen e butoni “Hape”, nëse s’janë përzgjedhur kredenciale</string>
|
||||
<string name="about_description">Sendërtim për Android i përgjegjësit KeePass të fjalëkalimeve</string>
|
||||
|
||||
@@ -176,7 +176,6 @@
|
||||
<string name="sort_groups_before">முன் குழுக்கள்</string>
|
||||
<string name="warning_no_encryption_key">குறியாக்க விசை இல்லாமல் தொடரவா?</string>
|
||||
<string name="warning_permanently_delete_nodes">தேர்ந்தெடுக்கப்பட்ட முனைகளை நிரந்தரமாக நீக்கவா?</string>
|
||||
<string name="advanced_unlock_scanning_error">சாதனம் திறத்தல் பிழை: %1$s</string>
|
||||
<string name="device_credential_unlock_enable_summary">தரவுத்தளத்தைத் திறக்க உங்கள் சாதன நற்சான்றிதழைப் பயன்படுத்தலாம்</string>
|
||||
<string name="biometric_delete_all_key_title">குறியாக்க விசைகளை நீக்கு</string>
|
||||
<string name="biometric_delete_all_key_summary">சாதன திறத்தல் ஏற்பு தொடர்பான அனைத்து குறியாக்க விசைகளையும் நீக்கு</string>
|
||||
|
||||
@@ -525,7 +525,6 @@
|
||||
<string name="advanced_unlock_invalid_key">อ่านกุญแจการปลดล็อกของอุปกรณ์ไม่ได้ โปรดลบข้อมูลออกและเพื่มข้อมูลการปลดล็อกด้วยอุปกรณ์อีกครั้ง</string>
|
||||
<string name="advanced_unlock_not_recognized">ไม่รู้จักลายนิ้วมือ</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">แยกข้อมูลประจำตัวออกด้วยข้อมูลการปลดล็อกด้วยอุปกรณ์</string>
|
||||
<string name="advanced_unlock_scanning_error">การปลดล็อกด้วยอุปกรณ์ผิดพลาด: %1$s</string>
|
||||
<string name="properties">คุณสมบัติ</string>
|
||||
<string name="unavailable">ฐานข้อมูลนี้ยังไม่มีข้อมูลการเข้าสูระบบเลย</string>
|
||||
<string name="database_history">ประวัติ</string>
|
||||
|
||||
@@ -488,7 +488,6 @@
|
||||
<string name="credential_before_click_advanced_unlock_button">Parolayı yazın ve ardından bu düğmeye tıklayın.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Cihaz kilit açma istemi başlatılamıyor.</string>
|
||||
<string name="unavailable">Kullanım dışı</string>
|
||||
<string name="advanced_unlock_scanning_error">Cihaz kilit açma hatası: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Cihaz kilit açma parmak izi tanınamadı</string>
|
||||
<string name="advanced_unlock_invalid_key">Cihazın kilit açma anahtarı okunamıyor. Lütfen silin ve kilit açma tanıma prosedürünü tekrarlayın.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Cihaz kilit açma verileriyle veritabanı kimlik bilgilerini çıkarın</string>
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
<string name="device_credential">Облікові дані пристрою</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Введіть пароль, а потім натисніть цю кнопку.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Не вдалося ініціалізувати запит на розблокування пристрою.</string>
|
||||
<string name="advanced_unlock_scanning_error">Помилка розблокування пристрою: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Не вдалося розпізнати розблокування пристрою</string>
|
||||
<string name="advanced_unlock_invalid_key">Не вдалося розпізнати ключ розблокування пристрою. Видаліть його й повторіть процедуру створення ключа.</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Витягування облікових даних бази даних за допомогою даних розблокування пристрою</string>
|
||||
|
||||
@@ -400,7 +400,6 @@
|
||||
<string name="encrypted_value_stored">Đã lưu trữ mật khẩu được mã hóa</string>
|
||||
<string name="advanced_unlock_invalid_key">Không thể đọc được mã mở khóa thiết bị. Vui lòng xóa nó và lặp lại quy trình nhận dạng mở khóa.</string>
|
||||
<string name="advanced_unlock_not_recognized">Không thể nhận dạng vân tay mở khóa thiết bị</string>
|
||||
<string name="advanced_unlock_scanning_error">Lỗi mở khóa thiết bị: %1$s</string>
|
||||
<string name="unavailable">Không có sẵn</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Không thể khởi tạo lời nhắc mở khóa thiết bị.</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Nhập mật khẩu rồi nhấp vào nút này.</string>
|
||||
|
||||
@@ -492,7 +492,6 @@
|
||||
<string name="device_credential">设备凭据</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">输入密码,然后点击这个按钮。</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">无法初始化设备解锁提示。</string>
|
||||
<string name="advanced_unlock_scanning_error">设备解锁出错:%1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">无法识别设备解锁印记</string>
|
||||
<string name="advanced_unlock_invalid_key">无法读取设备解锁密钥。请删除它,并重复解锁识别步骤。</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">用设备解锁数据提取数据库凭据</string>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<string name="advanced_unlock_prompt_not_initialized">無法初始化裝置解鎖提示。</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_message">即使你使用裝置解鎖識別,你仍然需要記住你的解鎖憑證。</string>
|
||||
<string name="advanced_unlock_prompt_store_credential_title">裝置解鎖連線</string>
|
||||
<string name="advanced_unlock_scanning_error">裝置解鎖出錯:%1$s</string>
|
||||
<string name="advanced_unlock_tap_delete">點擊刪除裝置解鎖密鑰</string>
|
||||
<string name="advanced_unlock_timeout">裝置解鎖超時</string>
|
||||
<string name="allow">允許</string>
|
||||
|
||||
@@ -407,7 +407,6 @@
|
||||
<string name="encrypted_value_stored">Encrypted password stored</string>
|
||||
<string name="advanced_unlock_invalid_key">Cannot read the device unlock key. Please delete it and repeat the unlock recognition procedure.</string>
|
||||
<string name="advanced_unlock_not_recognized">Could not recognize device unlock print</string>
|
||||
<string name="advanced_unlock_scanning_error">Device unlock error: %1$s</string>
|
||||
<string name="unavailable">Unavailable</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Unable to initialize device unlock prompt.</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Type in the password, and then click this button.</string>
|
||||
|
||||
@@ -10,7 +10,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.11.0'
|
||||
classpath 'com.android.tools.build:gradle:8.11.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,6 @@ dependencies {
|
||||
// Crypto
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
|
||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||
testImplementation "androidx.test:runner:$android_test_version"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
* Fix Autofill Registration #2089
|
||||
* Fix Biometric errors #2081
|
||||
* Fixed timestamp in copy file #1981 #1983
|
||||
* Fix Template Email #1986
|
||||
* Fix Template Email #1986
|
||||
* Fix Search #2096
|
||||
3
fastlane/metadata/android/en-US/changelogs/136.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/136.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
* Fix UnlockManager #2098 #2101
|
||||
* Auto device unlock prompt #2105
|
||||
* Small fixes ##2066
|
||||
@@ -1,4 +1,5 @@
|
||||
* Correction de l'enregistrement pour le remplissage automatique #2089
|
||||
* Correction des erreurs biométriques #2081
|
||||
* Correction du timestamp dans le fichier de copie #1981 #1983
|
||||
* Correction des gabaris Email #1986
|
||||
* Correction des gabaris Email #1986
|
||||
* Correction de la recherche #2096
|
||||
3
fastlane/metadata/android/fr-FR/changelogs/136.txt
Normal file
3
fastlane/metadata/android/fr-FR/changelogs/136.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
* Correction UnlockManager #2098 #2101
|
||||
* Invite de déverrouillage automatique de l'appareil #2105
|
||||
* Petites corrections ##2066
|
||||
Reference in New Issue
Block a user