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)
|
KeePassDX(4.1.3)
|
||||||
* Fix Autofill Registration #2089
|
* Fix Autofill Registration #2089
|
||||||
* Fix Biometric errors #2081
|
* Fix Biometric errors #2081
|
||||||
* Fixed timestamp in copy file #1981 #1983
|
* Fixed timestamp in copy file #1981 #1983
|
||||||
* Fix Template Email #1986
|
* Fix Template Email #1986
|
||||||
|
* Fix Search #2096
|
||||||
|
|
||||||
KeePassDX(4.1.2)
|
KeePassDX(4.1.2)
|
||||||
* Fix URL search #1940 #1946 #2003 #2040 #2044
|
* 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)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1009.0)
|
aws-partitions (1.1146.0)
|
||||||
aws-sdk-core (3.213.0)
|
aws-sdk-core (3.229.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.95.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-kms (1.110.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.171.0)
|
aws-sdk-s3 (1.196.1)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.1)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.2.0)
|
base64 (0.3.0)
|
||||||
|
bigdecimal (3.2.2)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.6.20240107)
|
domain_name (0.6.20240107)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
@@ -55,11 +59,11 @@ GEM
|
|||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (~> 1.0.0)
|
||||||
faraday-em_http (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-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.1.1)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.2)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
@@ -67,8 +71,8 @@ GEM
|
|||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.1)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.3.1)
|
fastimage (2.4.0)
|
||||||
fastlane (2.225.0)
|
fastlane (2.228.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
@@ -108,7 +112,7 @@ GEM
|
|||||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
word_wrap (~> 1.0.0)
|
word_wrap (~> 1.0.0)
|
||||||
xcodeproj (>= 1.13.0, < 2.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)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-plugin-versioning_android (0.1.1)
|
fastlane-plugin-versioning_android (0.1.1)
|
||||||
fastlane-sirp (1.0.0)
|
fastlane-sirp (1.0.0)
|
||||||
@@ -130,12 +134,12 @@ GEM
|
|||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.31.0)
|
google-apis-storage_v1 (0.31.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
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-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.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)
|
google-cloud-storage (1.47.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
@@ -151,36 +155,39 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.7)
|
http-cookie (1.0.8)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.8.2)
|
json (2.13.2)
|
||||||
jwt (2.9.3)
|
jwt (2.10.2)
|
||||||
base64
|
base64
|
||||||
|
logger (1.7.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
multi_json (1.15.0)
|
multi_json (1.17.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
|
mutex_m (0.3.0)
|
||||||
nanaimo (0.4.0)
|
nanaimo (0.4.0)
|
||||||
naturally (2.2.1)
|
naturally (2.3.0)
|
||||||
nkf (0.2.0)
|
nkf (0.2.0)
|
||||||
optparse (0.6.0)
|
optparse (0.6.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.1)
|
plist (3.7.2)
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.2)
|
||||||
rake (13.2.1)
|
rake (13.3.0)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.3.9)
|
rexml (3.4.1)
|
||||||
rouge (2.0.7)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.4.1)
|
||||||
security (0.1.5)
|
security (0.1.5)
|
||||||
signet (0.19.0)
|
signet (0.20.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
@@ -207,8 +214,8 @@ GEM
|
|||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.4.0)
|
nanaimo (~> 0.4.0)
|
||||||
rexml (>= 3.3.6, < 4.0)
|
rexml (>= 3.3.6, < 4.0)
|
||||||
xcpretty (0.3.0)
|
xcpretty (0.4.1)
|
||||||
rouge (~> 2.0.7)
|
rouge (~> 3.28.0)
|
||||||
xcpretty-travis-formatter (1.0.1)
|
xcpretty-travis-formatter (1.0.1)
|
||||||
xcpretty (~> 0.2, >= 0.0.7)
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
@@ -220,4 +227,4 @@ DEPENDENCIES
|
|||||||
fastlane-plugin-versioning_android
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.10
|
2.6.9
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode = 135
|
versionCode = 136
|
||||||
versionName = "4.1.3"
|
versionName = "4.1.4"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null, null)
|
launchPasswordActivity(databaseUri, null, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
overridePendingTransition(0, 0)
|
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.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
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.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
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.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_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.MainCredentialView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockState
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
@@ -98,10 +104,10 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||||
|
|
||||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels()
|
||||||
|
|
||||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
@@ -170,8 +176,9 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||||
mAdvancedUnlockViewModel.checkUnlockAvailability(
|
mDeviceUnlockViewModel.checkConditionToStoreCredential(
|
||||||
conditionToStoreCredentialVerified = verified
|
condition = verified,
|
||||||
|
databaseFileUri = mDatabaseFileUri
|
||||||
)
|
)
|
||||||
// TODO Async by ViewModel
|
// TODO Async by ViewModel
|
||||||
enableConfirmationButton()
|
enableConfirmationButton()
|
||||||
@@ -226,20 +233,31 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
mAdvancedUnlockViewModel.uiState.collect { uiState ->
|
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||||
// New value received
|
// New value received
|
||||||
if (uiState.isCredentialRequired) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mAdvancedUnlockViewModel.provideCredentialForEncryption(
|
uiState.credentialRequiredCipher?.let { cipher ->
|
||||||
getCredentialForEncryption()
|
mDeviceUnlockViewModel.encryptCredential(
|
||||||
)
|
credential = getCredentialForEncryption(),
|
||||||
|
cipher = cipher
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||||
onCredentialEncrypted(cipherEncryptDatabase)
|
onCredentialEncrypted(cipherEncryptDatabase)
|
||||||
mAdvancedUnlockViewModel.consumeCredentialEncrypted()
|
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
||||||
}
|
}
|
||||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||||
onCredentialDecrypted(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()
|
super.onResume()
|
||||||
|
|
||||||
// Init Biometric elements only if allowed
|
// Init Biometric elements only if allowed
|
||||||
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
advancedUnlockFragment = supportFragmentManager
|
&& PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
deviceUnlockFragment = supportFragmentManager
|
||||||
if (advancedUnlockFragment == null) {
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||||
advancedUnlockFragment = AdvancedUnlockFragment().also {
|
if (deviceUnlockFragment == null) {
|
||||||
|
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
replace(
|
replace(
|
||||||
R.id.fragment_advanced_unlock_container_view,
|
R.id.fragment_advanced_unlock_container_view,
|
||||||
@@ -274,11 +293,6 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
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 ->
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
}
|
}
|
||||||
@@ -402,6 +416,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
// Check if database really loaded
|
// Check if database really loaded
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
|
mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true
|
||||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||||
GroupActivity.launch(this,
|
GroupActivity.launch(this,
|
||||||
database,
|
database,
|
||||||
@@ -494,7 +509,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
loadDatabase()
|
loadDatabase()
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
mDeviceUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
enableConfirmationButton()
|
enableConfirmationButton()
|
||||||
@@ -524,8 +539,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Reinit locking activity UI variable
|
// Reinit locking activity UI variable
|
||||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
UI_VISIBLE_DURING_LOCK = false
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +668,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
&& !readOnlyEducationPerformed) {
|
&& !readOnlyEducationPerformed) {
|
||||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockButton != null) {
|
&& advancedUnlockButton != null) {
|
||||||
|
|||||||
@@ -47,10 +47,14 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
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.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||||
PasswordEncodingDialogFragment.Listener {
|
PasswordEncodingDialogFragment.Listener {
|
||||||
@@ -184,8 +188,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mLockReceiver = LockReceiver {
|
mLockReceiver = LockReceiver {
|
||||||
mDatabase = null
|
mDatabase = null
|
||||||
closeDatabase(database)
|
closeDatabase(database)
|
||||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
UI_VISIBLE_DURING_LOCK = UI_VISIBLE
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
|
||||||
mExitLock = true
|
mExitLock = true
|
||||||
closeOptionsMenu()
|
closeOptionsMenu()
|
||||||
finish()
|
finish()
|
||||||
@@ -414,7 +417,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
UI_VISIBLE = true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||||
@@ -429,7 +432,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
UI_VISIBLE = false
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
@@ -481,8 +484,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
var UI_VISIBLE: Boolean = false
|
||||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
var UI_VISIBLE_DURING_LOCK: Boolean = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.stylish
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -77,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
finish()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -177,14 +177,18 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCipherDatabase(databaseUri: Uri,
|
fun containsCipherDatabase(databaseUri: Uri?,
|
||||||
contains: (Boolean) -> Unit) {
|
contains: (Boolean) -> Unit) {
|
||||||
getCipherDatabase(databaseUri) {
|
if (databaseUri == null) {
|
||||||
contains.invoke(it != null)
|
contains.invoke(false)
|
||||||
|
} else {
|
||||||
|
getCipherDatabase(databaseUri) {
|
||||||
|
contains.invoke(it != null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetCipherParameters(databaseUri: Uri) {
|
fun resetCipherParameters(databaseUri: Uri?) {
|
||||||
containsCipherDatabase(databaseUri) { contains ->
|
containsCipherDatabase(databaseUri) { contains ->
|
||||||
if (contains) {
|
if (contains) {
|
||||||
mBinder?.resetTimer()
|
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.dialogs.UnavailableFeatureDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
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.education.Education
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
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 tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
||||||
|
|
||||||
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.biometricUnlockSupported(activity)
|
DeviceUnlockManager.biometricUnlockSupported(activity)
|
||||||
} else false
|
} else false
|
||||||
biometricUnlockEnablePreference?.apply {
|
biometricUnlockEnablePreference?.apply {
|
||||||
// False if under Marshmallow
|
// False if under Marshmallow
|
||||||
@@ -296,7 +296,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
|
DeviceUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||||
} else false
|
} else false
|
||||||
deviceCredentialUnlockEnablePreference?.apply {
|
deviceCredentialUnlockEnablePreference?.apply {
|
||||||
// Biometric unlock already checked
|
// Biometric unlock already checked
|
||||||
@@ -395,7 +395,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
validate?.invoke()
|
validate?.invoke()
|
||||||
warningAlertDialog?.setOnDismissListener(null)
|
warningAlertDialog?.setOnDismissListener(null)
|
||||||
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
DeviceUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
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.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
@@ -512,7 +512,7 @@ object PreferencesUtil {
|
|||||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||||
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
||||||
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.biometricUnlockSupported(context)
|
DeviceUnlockManager.biometricUnlockSupported(context)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
class DeviceUnlockView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: LinearLayout(context, attrs, defStyle) {
|
: LinearLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
private var biometricButtonView: Button? = null
|
private var biometricButtonView: Button? = null
|
||||||
@@ -45,7 +44,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
|||||||
biometricButtonView = findViewById(R.id.biometric_button)
|
biometricButtonView = findViewById(R.id.biometric_button)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setIconViewClickListener(listener: OnClickListener?) {
|
fun setDeviceUnlockButtonViewClickListener(listener: OnClickListener?) {
|
||||||
biometricButtonView?.setOnClickListener(listener)
|
biometricButtonView?.setOnClickListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +59,4 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
|||||||
fun setTitle(@StringRes textId: Int) {
|
fun setTitle(@StringRes textId: Int) {
|
||||||
title = context.getString(textId)
|
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"?>
|
<?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"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/advanced_unlock_view"
|
android:id="@+id/advanced_unlock_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -648,7 +648,6 @@
|
|||||||
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
||||||
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
||||||
<string name="advanced_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</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="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
||||||
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||||
<string name="device_credential_unlock_enable_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="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="advanced_unlock_prompt_store_credential_title">Cihaz kilidini açma linki</string>
|
||||||
<string name="database_history">Tarixçə</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_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_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>
|
<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_title">Използване на кошчето</string>
|
||||||
<string name="recycle_bin_summary">Премества групите и записите в групата „Кошче“ вместо да ги премахва директно</string>
|
<string name="recycle_bin_summary">Премества групите и записите в групата „Кошче“ вместо да ги премахва директно</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_title">Отключване с устройството</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_not_recognized">Не може да бъде разпознато кога устройството е отключено</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Заявката за отключване не може да бъде подготвена.</string>
|
<string name="advanced_unlock_prompt_not_initialized">Заявката за отключване не може да бъде подготвена.</string>
|
||||||
<string name="password_size_summary">Подразбирана дължина на създаваните пароли</string>
|
<string name="password_size_summary">Подразбирана дължина на създаваните пароли</string>
|
||||||
|
|||||||
@@ -613,7 +613,6 @@
|
|||||||
<string name="configure">Configura</string>
|
<string name="configure">Configura</string>
|
||||||
<string name="biometric_security_update_required">Cal actualitzar la seguretat biomètrica.</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="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="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="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>
|
<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="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="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_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_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_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>
|
<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="content">Indhold</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">Indtast adgangskoden, og klik derefter på denne knap.</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_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_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_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>
|
<string name="advanced_unlock_prompt_extract_credential_title">Enhedsoplåsningsgenkendelse</string>
|
||||||
|
|||||||
@@ -529,7 +529,6 @@
|
|||||||
<string name="device_credential">Geräteanmeldedaten</string>
|
<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="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_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_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_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>
|
<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="credential_before_click_advanced_unlock_button">Πληκτρολογήστε τον κωδικό πρόσβασης, και στη συνέχεια κάντε κλικ αυτό το κουμπί.</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Δεν είναι δυνατή η προετοιμασία της προτροπής ξεκλειδώματος συσκευής.</string>
|
<string name="advanced_unlock_prompt_not_initialized">Δεν είναι δυνατή η προετοιμασία της προτροπής ξεκλειδώματος συσκευής.</string>
|
||||||
<string name="advanced_unlock_not_recognized">Δεν ήταν δυνατή η αναγνώριση αποτυπώματος ξεκλειδώματος συσκευής</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_invalid_key">Δεν είναι δυνατή η ανάγνωση του κλειδιού ξεκλειδώματος της συσκευής. Διαγράψτε το και επαναλάβετε τη διαδικασία αναγνώρισης ξεκλειδώματος.</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">Εξαγωγή διαπιστευτηρίων βάσης δεδομένων με δεδομένα ξεκλειδώματος συσκευής</string>
|
<string name="advanced_unlock_prompt_extract_credential_message">Εξαγωγή διαπιστευτηρίων βάσης δεδομένων με δεδομένα ξεκλειδώματος συσκευής</string>
|
||||||
<string name="education_advanced_unlock_summary">Συνδέστε τον κωδικό πρόσβασής σας με το σαρωμένο βιομετρικό ή τα διαπιστευτήρια της συσκευής σας για να ξεκλειδώσετε γρήγορα τη βάση δεδομένων σας.</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="colorize_password_summary">Colourise password characters by type</string>
|
||||||
<string name="content_description_entry_background_color">Entry background colour</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="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_not_recognized">Could not recognise device unlock print</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Unable to initialise advanced unlock prompt.</string>
|
<string name="advanced_unlock_prompt_not_initialized">Unable to initialise device unlock prompt.</string>
|
||||||
<string name="download_initialization">Initialising…</string>
|
<string name="download_initialization">Initialising…</string>
|
||||||
<string name="download_finalization">Finalising…</string>
|
<string name="download_finalization">Finalising…</string>
|
||||||
<string name="download_canceled">Cancelled!</string>
|
<string name="download_canceled">Cancelled!</string>
|
||||||
|
|||||||
@@ -444,7 +444,6 @@
|
|||||||
<string name="device_credential">Credenciales del dispositivo</string>
|
<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="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_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_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_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>
|
<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="merge_success">Mestimine õnnestus</string>
|
||||||
<string name="biometric_security_update_required">Vajalik on biomeetrilise turvalisuse uuendus.</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="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_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="advanced_unlock_prompt_not_initialized">Seadme lukustuse eemaldamise päringu käivitamine ei õnnestu.</string>
|
||||||
<string name="biometric">Biomeetriline</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="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="encrypted_value_stored">Zifratutako pasahitza gordeta</string>
|
||||||
<string name="unavailable">Datu-base honek ez du biltegiratuta kredentzialik.</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="menu_appearance_settings">Itxura</string>
|
||||||
<string name="autofill_sign_in_prompt">KeePassDXekin erregistratu</string>
|
<string name="autofill_sign_in_prompt">KeePassDXekin erregistratu</string>
|
||||||
<string name="autofill_explanation_summary">Gaitu betetze automatikoa beste aplikazioetako formularioak errez betetzeko</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="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="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_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_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_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>
|
<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="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="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_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="autofill_service_name">Servizo de autocompletado do KeePassDX</string>
|
||||||
<string name="database_history">Historial</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>
|
<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="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_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_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_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_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>
|
<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="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="temp_advanced_unlock_enable_title">Ideiglenes eszközfeloldás</string>
|
||||||
<string name="permission">Engedély</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="content">Tartalom</string>
|
||||||
<string name="advanced_unlock_tap_delete">Koppintson az eszközfeloldási kulcsok törléséhez</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>
|
<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="upper_case">HURUF BESAR</string>
|
||||||
<string name="title_case">Huruf Judul</string>
|
<string name="title_case">Huruf Judul</string>
|
||||||
<string name="character_count">Jumlah karakter: %1$d</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="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="monospace_font_fields_enable_title">Bidang tipe huruf</string>
|
||||||
<string name="keyboard_save_search_info_title">Simpan info terbagi</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="advanced_unlock_prompt_store_credential_title">Collegamento allo sblocco con dispositivo</string>
|
||||||
<string name="device_credential">Credenziali del 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="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="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="menu_keystore_remove_key">Elimina chiave di sblocco del dispositivo</string>
|
||||||
<string name="error_rebuild_list">Non è possibile ricostruire la lista correttamente.</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_prompt_extract_credential_message">חלץ אישור מסד נתונים עם נתוני ביטול נעילת מכשיר</string>
|
||||||
<string name="advanced_unlock_invalid_key">לא ניתן לקרוא את מפתח ביטול נעילת המכשיר. נא למחוק אותו ולחזור על התהליך לזיהוי ביטול נעילה.</string>
|
<string name="advanced_unlock_invalid_key">לא ניתן לקרוא את מפתח ביטול נעילת המכשיר. נא למחוק אותו ולחזור על התהליך לזיהוי ביטול נעילה.</string>
|
||||||
<string name="advanced_unlock_not_recognized">לא היה ניתן לזהות טביעת ביטול נעילת מכשיר</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="credential_before_click_advanced_unlock_button">הקלד את הסיסמה, ואז לחץ על הכפתור הזה.</string>
|
||||||
<string name="lock_database_show_button_title">הצג כפתור נעילה</string>
|
<string name="lock_database_show_button_title">הצג כפתור נעילה</string>
|
||||||
<string name="lock_database_show_button_summary">הצג את כפתור הנעילה בממשק המשתמש</string>
|
<string name="lock_database_show_button_summary">הצג את כפתור הנעילה בממשק המשתמש</string>
|
||||||
|
|||||||
@@ -491,7 +491,6 @@
|
|||||||
<string name="device_credential">デバイス認証情報</string>
|
<string name="device_credential">デバイス認証情報</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">パスワードを入力し、このボタンをタップします。</string>
|
<string name="credential_before_click_advanced_unlock_button">パスワードを入力し、このボタンをタップします。</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">デバイスのロック解除プロンプトを初期化できません。</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_invalid_key">デバイスのロック解除キーを読み取ることができません。削除して、ロック解除認識手順を繰り返してください。</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">デバイスのロック解除データを使用してデータベースの資格情報を抽出する</string>
|
<string name="advanced_unlock_prompt_extract_credential_message">デバイスのロック解除データを使用してデータベースの資格情報を抽出する</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_title">デバイスのロック解除認識</string>
|
<string name="advanced_unlock_prompt_extract_credential_title">デバイスのロック解除認識</string>
|
||||||
|
|||||||
@@ -395,7 +395,6 @@
|
|||||||
<string name="configure">Konfigūruoti</string>
|
<string name="configure">Konfigūruoti</string>
|
||||||
<string name="biometric_security_update_required">Reikalingas biometrinių duomenų saugumo atnaujinimas.</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_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="database_history">Istorija</string>
|
||||||
<string name="properties">Nustatymai</string>
|
<string name="properties">Nustatymai</string>
|
||||||
<string name="menu_appearance_settings_summary">Temos, spalvos, atributai</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_summary">Skjul ødelagte lenker i listen over nylige databaser</string>
|
||||||
<string name="hide_broken_locations_title">Skjul ødelagte databaselenker</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="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_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="warning_sure_add_file">Legg til filen uansett\?</string>
|
||||||
<string name="registration_mode">Registreringsmodus</string>
|
<string name="registration_mode">Registreringsmodus</string>
|
||||||
|
|||||||
@@ -508,7 +508,6 @@
|
|||||||
<string name="device_credential">Apparaatreferentie</string>
|
<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="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_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_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_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>
|
<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="advanced_unlock_tap_delete">Stuknij, aby usunąć klucze odblokowywania urządzenia</string>
|
||||||
<string name="content">Zawartość</string>
|
<string name="content">Zawartość</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_title">Rozpoznawanie odblokowania urządzenia</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_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="error_database_uri_null">Nie można pobrać identyfikatora URI bazy danych.</string>
|
||||||
<string name="autofill_inline_suggestions_keyboard">Dodano sugestie autouzupełniania.</string>
|
<string name="autofill_inline_suggestions_keyboard">Dodano sugestie autouzupełniania.</string>
|
||||||
|
|||||||
@@ -512,7 +512,6 @@
|
|||||||
<string name="properties">Propriedades</string>
|
<string name="properties">Propriedades</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">Digite a senha e clique neste botão.</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_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_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_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>
|
<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="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="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_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_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_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>
|
<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="accept">Aceitar</string>
|
||||||
<string name="device_credential">Credencial do dispositivo</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_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_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_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>
|
<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="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_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_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="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="configure_biometric">Nu se înrolează nicio credențială biometrică sau de dispozitiv.</string>
|
||||||
<string name="autofill_select_entry">Selectați intrarea…</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_extract_credential_title">Распознавание разблокировки устройства</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_message">При использовании разблокировки устройства вам всё равно необходимо помнить основные учётные данные.</string>
|
<string name="advanced_unlock_prompt_store_credential_message">При использовании разблокировки устройства вам всё равно необходимо помнить основные учётные данные.</string>
|
||||||
<string name="advanced_unlock_delete_all_key_warning">Удалить все ключи шифрования, связанные с распознаванием разблокировки устройства\?</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="advanced_unlock_prompt_store_credential_title">Настройка разблокировки устройства</string>
|
||||||
<string name="menu_keystore_remove_key">Удалить ключ разблокировки устройства</string>
|
<string name="menu_keystore_remove_key">Удалить ключ разблокировки устройства</string>
|
||||||
<string name="enter">Ввод</string>
|
<string name="enter">Ввод</string>
|
||||||
|
|||||||
@@ -476,7 +476,6 @@
|
|||||||
<string name="style_choose_title">Téma aplikácie</string>
|
<string name="style_choose_title">Téma aplikácie</string>
|
||||||
<string name="protection">Ochrana</string>
|
<string name="protection">Ochrana</string>
|
||||||
<string name="configure_biometric">Nie sú zaregistrované žiadne biometrické údaje ani poverenia zariadenia.</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="lock">Zamknúť</string>
|
||||||
<string name="custom_fields">Vlastné polia</string>
|
<string name="custom_fields">Vlastné polia</string>
|
||||||
<string name="education_sort_summary">Vyberte spôsob triedenia záznamov a skupín.</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="show_entry_colors_title">Ngjyra zërash</string>
|
||||||
<string name="hide_expired_entries_title">Fshihi zërat e skaduar</string>
|
<string name="hide_expired_entries_title">Fshihi zërat e skaduar</string>
|
||||||
<string name="error_XML_malformed">XML e keqformuar.</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="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="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>
|
<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="sort_groups_before">முன் குழுக்கள்</string>
|
||||||
<string name="warning_no_encryption_key">குறியாக்க விசை இல்லாமல் தொடரவா?</string>
|
<string name="warning_no_encryption_key">குறியாக்க விசை இல்லாமல் தொடரவா?</string>
|
||||||
<string name="warning_permanently_delete_nodes">தேர்ந்தெடுக்கப்பட்ட முனைகளை நிரந்தரமாக நீக்கவா?</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="device_credential_unlock_enable_summary">தரவுத்தளத்தைத் திறக்க உங்கள் சாதன நற்சான்றிதழைப் பயன்படுத்தலாம்</string>
|
||||||
<string name="biometric_delete_all_key_title">குறியாக்க விசைகளை நீக்கு</string>
|
<string name="biometric_delete_all_key_title">குறியாக்க விசைகளை நீக்கு</string>
|
||||||
<string name="biometric_delete_all_key_summary">சாதன திறத்தல் ஏற்பு தொடர்பான அனைத்து குறியாக்க விசைகளையும் நீக்கு</string>
|
<string name="biometric_delete_all_key_summary">சாதன திறத்தல் ஏற்பு தொடர்பான அனைத்து குறியாக்க விசைகளையும் நீக்கு</string>
|
||||||
|
|||||||
@@ -525,7 +525,6 @@
|
|||||||
<string name="advanced_unlock_invalid_key">อ่านกุญแจการปลดล็อกของอุปกรณ์ไม่ได้ โปรดลบข้อมูลออกและเพื่มข้อมูลการปลดล็อกด้วยอุปกรณ์อีกครั้ง</string>
|
<string name="advanced_unlock_invalid_key">อ่านกุญแจการปลดล็อกของอุปกรณ์ไม่ได้ โปรดลบข้อมูลออกและเพื่มข้อมูลการปลดล็อกด้วยอุปกรณ์อีกครั้ง</string>
|
||||||
<string name="advanced_unlock_not_recognized">ไม่รู้จักลายนิ้วมือ</string>
|
<string name="advanced_unlock_not_recognized">ไม่รู้จักลายนิ้วมือ</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">แยกข้อมูลประจำตัวออกด้วยข้อมูลการปลดล็อกด้วยอุปกรณ์</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="properties">คุณสมบัติ</string>
|
||||||
<string name="unavailable">ฐานข้อมูลนี้ยังไม่มีข้อมูลการเข้าสูระบบเลย</string>
|
<string name="unavailable">ฐานข้อมูลนี้ยังไม่มีข้อมูลการเข้าสูระบบเลย</string>
|
||||||
<string name="database_history">ประวัติ</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="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="advanced_unlock_prompt_not_initialized">Cihaz kilit açma istemi başlatılamıyor.</string>
|
||||||
<string name="unavailable">Kullanım dışı</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_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_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>
|
<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="device_credential">Облікові дані пристрою</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">Введіть пароль, а потім натисніть цю кнопку.</string>
|
<string name="credential_before_click_advanced_unlock_button">Введіть пароль, а потім натисніть цю кнопку.</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Не вдалося ініціалізувати запит на розблокування пристрою.</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_not_recognized">Не вдалося розпізнати розблокування пристрою</string>
|
||||||
<string name="advanced_unlock_invalid_key">Не вдалося розпізнати ключ розблокування пристрою. Видаліть його й повторіть процедуру створення ключа.</string>
|
<string name="advanced_unlock_invalid_key">Не вдалося розпізнати ключ розблокування пристрою. Видаліть його й повторіть процедуру створення ключа.</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">Витягування облікових даних бази даних за допомогою даних розблокування пристрою</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="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_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_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="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="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>
|
<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="device_credential">设备凭据</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">输入密码,然后点击这个按钮。</string>
|
<string name="credential_before_click_advanced_unlock_button">输入密码,然后点击这个按钮。</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">无法初始化设备解锁提示。</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_not_recognized">无法识别设备解锁印记</string>
|
||||||
<string name="advanced_unlock_invalid_key">无法读取设备解锁密钥。请删除它,并重复解锁识别步骤。</string>
|
<string name="advanced_unlock_invalid_key">无法读取设备解锁密钥。请删除它,并重复解锁识别步骤。</string>
|
||||||
<string name="advanced_unlock_prompt_extract_credential_message">用设备解锁数据提取数据库凭据</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_not_initialized">無法初始化裝置解鎖提示。</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_message">即使你使用裝置解鎖識別,你仍然需要記住你的解鎖憑證。</string>
|
<string name="advanced_unlock_prompt_store_credential_message">即使你使用裝置解鎖識別,你仍然需要記住你的解鎖憑證。</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_title">裝置解鎖連線</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_tap_delete">點擊刪除裝置解鎖密鑰</string>
|
||||||
<string name="advanced_unlock_timeout">裝置解鎖超時</string>
|
<string name="advanced_unlock_timeout">裝置解鎖超時</string>
|
||||||
<string name="allow">允許</string>
|
<string name="allow">允許</string>
|
||||||
|
|||||||
@@ -407,7 +407,6 @@
|
|||||||
<string name="encrypted_value_stored">Encrypted password stored</string>
|
<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_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_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="unavailable">Unavailable</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">Unable to initialize device unlock prompt.</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>
|
<string name="credential_before_click_advanced_unlock_button">Type in the password, and then click this button.</string>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
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"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,6 @@ dependencies {
|
|||||||
// Crypto
|
// Crypto
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||||
|
|
||||||
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
testImplementation "androidx.test:runner:$android_test_version"
|
testImplementation "androidx.test:runner:$android_test_version"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
* Fix Autofill Registration #2089
|
* Fix Autofill Registration #2089
|
||||||
* Fix Biometric errors #2081
|
* Fix Biometric errors #2081
|
||||||
* Fixed timestamp in copy file #1981 #1983
|
* 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 de l'enregistrement pour le remplissage automatique #2089
|
||||||
* Correction des erreurs biométriques #2081
|
* Correction des erreurs biométriques #2081
|
||||||
* Correction du timestamp dans le fichier de copie #1981 #1983
|
* 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