Merge branch 'develop' into feature/Merge_from

This commit is contained in:
J-Jamet
2022-02-12 00:29:19 +01:00
14 changed files with 736 additions and 315 deletions

View File

@@ -26,14 +26,14 @@ 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
import android.text.Editable
import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.view.* import android.view.Menu
import android.view.KeyEvent.KEYCODE_ENTER import android.view.MenuItem
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.ViewGroup
import android.widget.* import android.widget.Button
import android.widget.CompoundButton
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -44,10 +44,11 @@ import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.* import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
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.AdvancedUnlockFragment
@@ -55,11 +56,9 @@ import com.kunzisoft.keepass.database.element.Database
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.model.MainCredential import com.kunzisoft.keepass.model.*
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_ENTITY_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
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
@@ -68,7 +67,7 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
@@ -80,11 +79,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var filenameView: TextView? = null private var filenameView: TextView? = null
private var passwordView: EditText? = null private var mainCredentialView: MainCredentialView? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private var confirmButtonView: Button? = null private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = 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 advancedUnlockFragment: AdvancedUnlockFragment? = null
@@ -94,7 +90,6 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private var mDefaultDatabase: Boolean = false private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
@@ -118,12 +113,9 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
confirmButtonView = findViewById(R.id.activity_password_open_button)
filenameView = findViewById(R.id.filename) filenameView = findViewById(R.id.filename)
passwordView = findViewById(R.id.password) mainCredentialView = findViewById(R.id.activity_password_credentials)
keyFileSelectionView = findViewById(R.id.keyfile_selection) confirmButtonView = findViewById(R.id.activity_password_open_button)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
infoContainerView = findViewById(R.id.activity_password_info_container) infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
@@ -137,38 +129,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) { if (uri != null) {
mDatabaseKeyFileUri = uri mainCredentialView?.populateKeyFileTextView(uri)
populateKeyFileTextView(uri)
} }
} }
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
mainCredentialView?.onValidateListener = {
passwordView?.setOnEditorActionListener(onEditorActionListener) loadDatabase()
passwordView?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
})
passwordView?.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KEYCODE_ENTER) {
verifyCheckboxesAndLoadDatabase()
handled = true
}
handled
} }
// If is a view intent // If is a view intent
getUriFromIntent(intent) getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
// Init Biometric elements // Init Biometric elements
advancedUnlockFragment = supportFragmentManager advancedUnlockFragment = supportFragmentManager
@@ -183,10 +153,11 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
// Listen password checkbox to init advanced unlock and confirmation button // Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> mainCredentialView?.onPasswordChecked =
mAdvancedUnlockViewModel.checkUnlockAvailability() CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton() mAdvancedUnlockViewModel.checkUnlockAvailability()
} enableConfirmationButton()
}
// Observe if default database // Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -211,12 +182,13 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
invalidateOptionsMenu() invalidateOptionsMenu()
// Post init uri with KeyFile only if needed // Post init uri with KeyFile only if needed
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
val keyFileUri = val keyFileUri =
if (mRememberKeyFile if (mRememberKeyFile
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) { && (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
databaseFile?.keyFileUri databaseFile?.keyFileUri
} else { } else {
mDatabaseKeyFileUri databaseKeyFileUri
} }
// Define title // Define title
@@ -271,7 +243,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (result.isSuccess) { if (result.isSuccess) {
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} else { } else {
passwordView?.requestFocusFromTouch() mainCredentialView?.requestPasswordFocus()
var resultError = "" var resultError = ""
val resultException = result.exception val resultException = result.exception
@@ -288,7 +260,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
var databaseUri: Uri? = null var databaseUri: Uri? = null
var mainCredential = MainCredential() var mainCredential = MainCredential()
var readOnly = true var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null var cipherEncryptDatabase: CipherEncryptDatabase? = null
result.data?.let { resultData -> result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY) databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
@@ -296,8 +268,8 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
resultData.getParcelable(MAIN_CREDENTIAL_KEY) resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential ?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY) readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = cipherEncryptDatabase =
resultData.getParcelable(CIPHER_ENTITY_KEY) resultData.getParcelable(CIPHER_DATABASE_KEY)
} }
databaseUri?.let { databaseFileUri -> databaseUri?.let { databaseFileUri ->
@@ -305,7 +277,7 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
databaseFileUri, databaseFileUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherEntity, cipherEncryptDatabase,
true true
) )
} }
@@ -341,11 +313,16 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
if (action != null if (action != null
&& action == VIEW_INTENT) { && action == VIEW_INTENT) {
mDatabaseFileUri = intent.data mDatabaseFileUri = intent.data
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE) mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
} else { } else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
mainCredentialView?.populateKeyFileTextView(it)
}
} }
try {
intent?.removeExtra(KEY_KEYFILE)
} catch (e: Exception) {}
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
@@ -380,51 +357,68 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
finish() finish()
} }
override fun retrieveCredentialForEncryption(): String { override fun retrieveCredentialForEncryption(): ByteArray {
return passwordView?.text?.toString() ?: "" return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
?: byteArrayOf()
} }
override fun conditionToStoreCredential(): Boolean { override fun conditionToStoreCredential(): Boolean {
return checkboxPasswordView?.isChecked == true return mainCredentialView?.conditionToStoreCredential() == true
} }
override fun onCredentialEncrypted(databaseUri: Uri, override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
encryptedCredential: String,
ivSpec: String) {
// Load the database if password is registered with biometric // Load the database if password is registered with biometric
verifyCheckboxesAndLoadDatabase( loadDatabase(mDatabaseFileUri,
CipherDatabaseEntity( mainCredentialView?.getMainCredential(),
databaseUri.toString(), cipherEncryptDatabase
encryptedCredential,
ivSpec)
) )
} }
override fun onCredentialDecrypted(databaseUri: Uri, private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
decryptedCredential: String) { override fun passwordToStore(password: String?): ByteArray? {
// Load the database if password is retrieve from biometric return password?.toByteArray()
// Retrieve from biometric }
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
// TODO create byte array to store keyfile
return null
}
override fun hardwareKeyToStore(): ByteArray? {
// TODO create byte array to store hardware key
return null
}
} }
private val onEditorActionListener = object : TextView.OnEditorActionListener { override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { // Load the database if password is retrieve from biometric
if (actionId == IME_ACTION_DONE) { // Retrieve from biometric
verifyCheckboxesAndLoadDatabase() val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
return true when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
}
CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file
}
CredentialStorage.HARDWARE_KEY -> {
// TODO advanced unlock hardware key
} }
return false
} }
loadDatabase(mDatabaseFileUri,
mainCredential,
null
)
} }
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
// Define Key File text // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
populateKeyFileTextView(keyFileUri) mainCredentialView?.populateKeyFileTextView(keyFileUri)
} }
// Define listener for validate button // Define listener for validate button
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() } confirmButtonView?.setOnClickListener { loadDatabase() }
// If Activity is launch with a password and want to open directly // If Activity is launch with a password and want to open directly
val intent = intent val intent = intent
@@ -433,66 +427,33 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
intent.removeExtra(KEY_PASSWORD) intent.removeExtra(KEY_PASSWORD)
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false) val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
if (password != null) { if (password != null) {
populatePasswordTextView(password) mainCredentialView?.populatePasswordTextView(password)
} }
if (launchImmediately) { if (launchImmediately) {
verifyCheckboxesAndLoadDatabase(password, keyFileUri) loadDatabase()
} else { } else {
// Init Biometric elements // Init Biometric elements
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri) mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
} }
enableOrNotTheConfirmationButton() enableConfirmationButton()
// Auto select the password field and open keyboard mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
passwordView?.postDelayed({
passwordView?.requestFocusFromTouch()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
} }
private fun enableOrNotTheConfirmationButton() { private fun enableConfirmationButton() {
// Enable or not the open button if setting is checked // Enable or not the open button if setting is checked
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) { if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
checkboxPasswordView?.let { confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|| checkboxKeyFileView?.isChecked == true)
}
} else { } else {
confirmButtonView?.isEnabled = true confirmButtonView?.isEnabled = true
} }
} }
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
populatePasswordTextView(null) mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) { if (clearKeyFile) {
mDatabaseKeyFileUri = null mainCredentialView?.populateKeyFileTextView(null)
populateKeyFileTextView(null)
}
}
private fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView?.setText("")
if (checkboxPasswordView?.isChecked == true)
checkboxPasswordView?.isChecked = false
} else {
passwordView?.setText(text)
if (checkboxPasswordView?.isChecked != true)
checkboxPasswordView?.isChecked = true
}
}
private fun populateKeyFileTextView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView?.uri = null
if (checkboxKeyFileView?.isChecked == true)
checkboxKeyFileView?.isChecked = false
} else {
keyFileSelectionView?.uri = uri
if (checkboxKeyFileView?.isChecked != true)
checkboxKeyFileView?.isChecked = true
} }
} }
@@ -504,41 +465,20 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
outState.putBoolean(KEY_READ_ONLY, mReadOnly) outState.putBoolean(KEY_READ_ONLY, mReadOnly)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) { private fun loadDatabase() {
val password: String? = passwordView?.text?.toString() loadDatabase(mDatabaseFileUri,
val keyFile: Uri? = keyFileSelectionView?.uri mainCredentialView?.getMainCredential(),
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity) null
} )
private fun verifyCheckboxesAndLoadDatabase(password: String?,
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = keyFileSelectionView?.uri
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
} }
private fun loadDatabase(databaseFileUri: Uri?, private fun loadDatabase(databaseFileUri: Uri?,
password: String?, mainCredential: MainCredential?,
keyFileUri: Uri?, cipherEncryptDatabase: CipherEncryptDatabase?) {
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
clearCredentialsViews() clearCredentialsViews()
@@ -556,11 +496,12 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database // Show the progress dialog and load the database
showProgressDialogAndLoadDatabase( showProgressDialogAndLoadDatabase(
databaseUri, databaseUri,
MainCredential(password, keyFileUri), mainCredential ?: MainCredential(),
mReadOnly, mReadOnly,
cipherDatabaseEntity, cipherEncryptDatabase,
false) false
)
} }
} }
} }
@@ -568,14 +509,14 @@ class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderL
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUUID: Boolean) { fixDuplicateUUID: Boolean) {
loadDatabase( loadDatabase(
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherDatabaseEntity, cipherEncryptDatabase,
fixDuplicateUUID fixDuplicateUUID
) )
} }

View File

@@ -4,9 +4,9 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
@@ -59,9 +59,9 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
fun loadDatabase(databaseUri: Uri, fun loadDatabase(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid) mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
} }
protected fun closeDatabase() { protected fun closeDatabase() {

View File

@@ -22,7 +22,9 @@ package com.kunzisoft.keepass.app.database
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.util.Base64
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -125,15 +127,26 @@ class CipherDatabaseAction(context: Context) {
} }
fun getCipherDatabase(databaseUri: Uri, fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
if (useTempDao) { if (useTempDao) {
serviceActionTask { serviceActionTask {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) val cipherDatabaseEntity = mBinder?.getCipherDatabase(databaseUri)
val cipherDatabase = CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity?.databaseUri)
this.encryptedValue = Base64.decode(cipherDatabaseEntity?.encryptedValue, Base64.NO_WRAP)
this.specParameters = Base64.decode(cipherDatabaseEntity?.specParameters, Base64.NO_WRAP)
}
cipherDatabaseResultListener.invoke(cipherDatabase)
} }
} else { } else {
IOActionTask( IOActionTask(
{ {
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString()) val cipherDatabaseEntity = cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
CipherEncryptDatabase().apply {
this.databaseUri = Uri.parse(cipherDatabaseEntity?.databaseUri)
this.encryptedValue = Base64.decode(cipherDatabaseEntity?.encryptedValue, Base64.NO_WRAP)
this.specParameters = Base64.decode(cipherDatabaseEntity?.specParameters, Base64.NO_WRAP)
}
}, },
{ {
cipherDatabaseResultListener.invoke(it) cipherDatabaseResultListener.invoke(it)
@@ -149,18 +162,27 @@ class CipherDatabaseAction(context: Context) {
} }
} }
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
cipherDatabaseResultListener: (() -> Unit)? = null) { cipherDatabaseResultListener: (() -> Unit)? = null) {
if (useTempDao) { cipherEncryptDatabase.databaseUri?.let { databaseUri ->
// The only case to create service (not needed to get an info)
serviceActionTask(true) { val cipherDatabaseEntity = CipherDatabaseEntity(
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) databaseUri.toString(),
cipherDatabaseResultListener?.invoke() Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
} Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
} else { )
IOActionTask(
if (useTempDao) {
// The only case to create service (not needed to get an info)
serviceActionTask(true) {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{ {
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) val cipherDatabaseRetrieve =
cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database // Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) { if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity) cipherDatabaseDao.add(cipherDatabaseEntity)
@@ -171,7 +193,8 @@ class CipherDatabaseAction(context: Context) {
{ {
cipherDatabaseResultListener?.invoke() cipherDatabaseResultListener?.invoke()
} }
).execute() ).execute()
}
} }
} }

View File

@@ -40,6 +40,9 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
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.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
@@ -60,6 +63,9 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
var databaseFileUri: Uri? = null var databaseFileUri: Uri? = null
private set private set
// TODO Retrieve credential storage from app database
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
/** /**
* Manage setting to auto open biometric prompt * Manage setting to auto open biometric prompt
*/ */
@@ -477,6 +483,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: checkUnlockAvailability() } ?: checkUnlockAvailability()
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
@@ -528,16 +535,29 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) { override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
databaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec) mBuilderListener?.onCredentialEncrypted(
CipherEncryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.encryptedValue = encryptedValue
this.specParameters = ivSpec
}
)
} }
} }
override fun handleDecryptedResult(decryptedValue: String) { override fun handleDecryptedResult(decryptedValue: ByteArray) {
// Load database directly with password retrieve // Load database directly with password retrieve
databaseFileUri?.let { databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialDecrypted(it, decryptedValue) mBuilderListener?.onCredentialDecrypted(
CipherDecryptDatabase().apply {
this.databaseUri = databaseUri
this.credentialStorage = credentialDatabaseStorage
this.decryptedValue = decryptedValue
}
)
} }
} }
@@ -551,6 +571,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
} }
@RequiresApi(Build.VERSION_CODES.M)
override fun onGenericException(e: Exception) { override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage) setAdvancedUnlockedMessageView(errorMessage)
@@ -580,6 +601,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(text: CharSequence) { private fun setAdvancedUnlockedMessageView(text: CharSequence) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
mAdvancedUnlockInfoView?.message = text mAdvancedUnlockInfoView?.message = text
@@ -617,10 +639,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
interface BuilderListener { interface BuilderListener {
fun retrieveCredentialForEncryption(): String fun retrieveCredentialForEncryption(): ByteArray
fun conditionToStoreCredential(): Boolean fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String) fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String) fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
} }
override fun onPause() { override fun onPause() {

View File

@@ -27,7 +27,6 @@ import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -214,18 +213,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun encryptData(value: String) { fun encryptData(value: ByteArray) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
val encrypted = cipher?.doFinal(value.toByteArray()) val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
// passes updated iv spec on to callback so this can be stored for decryption // passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP) advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to encrypt data", e) Log.e(TAG, "Unable to encrypt data", e)
@@ -233,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun initDecryptData(ivSpecValue: String, fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true) initDecryptData(ivSpecValue, actionIfCypherInit, true)
} }
private fun initDecryptData(ivSpecValue: String, private fun initDecryptData(ivSpecValue: ByteArray,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit, actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) { firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
@@ -246,9 +242,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
try { try {
// important to restore spec here that was used for decryption // important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP) val spec = IvParameterSpec(ivSpecValue)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey -> getSecretKey()?.let { secretKey ->
cipher?.let { cipher -> cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
@@ -284,15 +278,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
} }
} }
fun decryptData(encryptedValue: String) { fun decryptData(encryptedValue: ByteArray) {
if (!isKeyManagerInitialized) { if (!isKeyManagerInitialized) {
return return
} }
try { try {
// actual decryption here // actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP) cipher?.doFinal(encryptedValue)?.let { decrypted ->
cipher?.doFinal(encrypted)?.let { decrypted -> advancedUnlockCallback?.handleDecryptedResult(decrypted)
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
} }
} catch (badPaddingException: BadPaddingException) { } catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException) Log.e(TAG, "Unable to decrypt data", badPaddingException)
@@ -367,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
fun onAuthenticationSucceeded() fun onAuthenticationSucceeded()
fun onAuthenticationFailed() fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence) fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String) fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
fun handleDecryptedResult(decryptedValue: String) fun handleDecryptedResult(decryptedValue: ByteArray)
} }
companion object { companion object {
@@ -469,9 +462,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {} override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
override fun handleDecryptedResult(decryptedValue: String) {} override fun handleDecryptedResult(decryptedValue: ByteArray) {}
override fun onUnrecoverableKeyException(e: Exception) { override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e) advancedCallback.onUnrecoverableKeyException(e)

View File

@@ -33,7 +33,6 @@ import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
@@ -43,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -343,13 +343,13 @@ class DatabaseTaskProvider {
fun startDatabaseLoad(databaseUri: Uri, fun startDatabaseLoad(databaseUri: Uri,
mainCredential: MainCredential, mainCredential: MainCredential,
readOnly: Boolean, readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?, cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean) { fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
} }
, ACTION_DATABASE_LOAD_TASK) , ACTION_DATABASE_LOAD_TASK)

View File

@@ -22,12 +22,11 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -39,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context,
private val mUri: Uri, private val mUri: Uri,
private val mMainCredential: MainCredential, private val mMainCredential: MainCredential,
private val mReadonly: Boolean, private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?, private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean, private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?, private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
@@ -76,9 +75,9 @@ class LoadDatabaseRunnable(private val context: Context,
} }
// Register the biometric // Register the biometric
mCipherEntity?.let { cipherDatabaseEntity -> mCipherEncryptDatabase?.let { cipherDatabase ->
CipherDatabaseAction.getInstance(context) CipherDatabaseAction.getInstance(context)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called .addOrUpdateCipherDatabase(cipherDatabase) // return value not called
} }
// Register the current time to init the lock timer // Register the current time to init the lock timer

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2022 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.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class CipherDecryptDatabase(): Parcelable {
var databaseUri: Uri? = null
var credentialStorage: CredentialStorage = CredentialStorage.DEFAULT
var decryptedValue: ByteArray = byteArrayOf()
constructor(parcel: Parcel): this() {
databaseUri = parcel.readParcelable(Uri::class.java.classLoader)
credentialStorage = parcel.readEnum<CredentialStorage>() ?: credentialStorage
decryptedValue = ByteArray(parcel.readInt())
parcel.readByteArray(decryptedValue)
}
fun replaceContent(copy: CipherDecryptDatabase) {
this.decryptedValue = copy.decryptedValue
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage)
parcel.writeInt(decryptedValue.size)
parcel.writeByteArray(decryptedValue)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CipherDecryptDatabase> {
override fun createFromParcel(parcel: Parcel): CipherDecryptDatabase {
return CipherDecryptDatabase(parcel)
}
override fun newArray(size: Int): Array<CipherDecryptDatabase?> {
return arrayOfNulls(size)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CipherDecryptDatabase
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri.hashCode()
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2022 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.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class CipherEncryptDatabase(): Parcelable {
var databaseUri: Uri? = null
var credentialStorage: CredentialStorage = CredentialStorage.DEFAULT
var encryptedValue: ByteArray = byteArrayOf()
var specParameters: ByteArray = byteArrayOf()
constructor(parcel: Parcel): this() {
databaseUri = parcel.readParcelable(Uri::class.java.classLoader)
credentialStorage = parcel.readEnum<CredentialStorage>() ?: credentialStorage
encryptedValue = ByteArray(parcel.readInt())
parcel.readByteArray(encryptedValue)
specParameters = ByteArray(parcel.readInt())
parcel.readByteArray(specParameters)
}
fun replaceContent(copy: CipherEncryptDatabase) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage)
parcel.writeInt(encryptedValue.size)
parcel.writeByteArray(encryptedValue)
parcel.writeInt(specParameters.size)
parcel.writeByteArray(specParameters)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<CipherEncryptDatabase> {
override fun createFromParcel(parcel: Parcel): CipherEncryptDatabase {
return CipherEncryptDatabase(parcel)
}
override fun newArray(size: Int): Array<CipherEncryptDatabase?> {
return arrayOfNulls(size)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CipherEncryptDatabase
if (databaseUri != other.databaseUri) return false
return true
}
override fun hashCode(): Int {
return databaseUri.hashCode()
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 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.model
enum class CredentialStorage {
PASSWORD, KEY_FILE, HARDWARE_KEY;
companion object {
fun getFromOrdinal(ordinal: Int): CredentialStorage {
return when (ordinal) {
0 -> PASSWORD
1 -> KEY_FILE
2 -> HARDWARE_KEY
else -> DEFAULT
}
}
val DEFAULT: CredentialStorage
get() = PASSWORD
}
}

View File

@@ -30,7 +30,6 @@ import android.util.Log
import androidx.media.app.NotificationCompat import androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.database.action.* import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
@@ -42,6 +41,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -474,7 +474,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
intent?.removeExtra(DATABASE_URI_KEY) intent?.removeExtra(DATABASE_URI_KEY)
intent?.removeExtra(MAIN_CREDENTIAL_KEY) intent?.removeExtra(MAIN_CREDENTIAL_KEY)
intent?.removeExtra(READ_ONLY_KEY) intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY) intent?.removeExtra(CIPHER_DATABASE_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
intent?.removeExtra(GROUP_KEY) intent?.removeExtra(GROUP_KEY)
intent?.removeExtra(ENTRY_KEY) intent?.removeExtra(ENTRY_KEY)
@@ -575,13 +575,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (intent.hasExtra(DATABASE_URI_KEY) if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(READ_ONLY_KEY) && intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY) && intent.hasExtra(CIPHER_DATABASE_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) { ) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) val cipherEncryptDatabase: CipherEncryptDatabase? = intent.getParcelableExtra(CIPHER_DATABASE_KEY)
if (databaseUri == null) if (databaseUri == null)
return null return null
@@ -594,7 +594,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, readOnly,
cipherEntity, cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this this
) { result -> ) { result ->
@@ -603,7 +603,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(READ_ONLY_KEY, readOnly) putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity) putParcelable(CIPHER_DATABASE_KEY, cipherEncryptDatabase)
} }
} }
} else { } else {
@@ -981,7 +981,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val DATABASE_URI_KEY = "DATABASE_URI_KEY" const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY" const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" const val CIPHER_DATABASE_KEY = "CIPHER_DATABASE_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
const val GROUP_KEY = "GROUP_KEY" const val GROUP_KEY = "GROUP_KEY"
const val ENTRY_KEY = "ENTRY_KEY" const val ENTRY_KEY = "ENTRY_KEY"

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2022 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.view
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.CredentialStorage
class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var passwordView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton
private var checkboxKeyFileView: CompoundButton
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onValidateListener: (() -> Unit)? = null
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_credentials, this)
passwordView = findViewById(R.id.password)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onValidateListener?.invoke()
return true
}
return false
}
}
passwordView.setOnEditorActionListener(onEditorActionListener)
passwordView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (editable.toString().isNotEmpty() && !checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = true
}
})
passwordView.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
) {
onValidateListener?.invoke()
handled = true
}
handled
}
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
onPasswordChecked?.onCheckedChanged(view, checked)
}
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
}
fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView.setText("")
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = false
} else {
passwordView.setText(text)
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = true
}
}
fun populateKeyFileTextView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView.uri = null
if (checkboxKeyFileView.isChecked)
checkboxKeyFileView.isChecked = false
} else {
keyFileSelectionView.uri = uri
if (!checkboxKeyFileView.isChecked)
checkboxKeyFileView.isChecked = true
}
}
fun isFill(): Boolean {
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked
}
fun getMainCredential(): MainCredential {
return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked)
passwordView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null
}
}
fun changeConditionToStoreCredential(credentialStorage: CredentialStorage) {
this.mCredentialStorage = credentialStorage
}
fun conditionToStoreCredential(): Boolean {
// TODO HARDWARE_KEY
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked
CredentialStorage.HARDWARE_KEY -> false
}
}
/**
* Return content of the store credential view allowed,
* String? for password
*
*/
fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? {
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordView.text?.toString())
CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri)
CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore()
}
}
interface CredentialStorageListener {
fun passwordToStore(password: String?): ByteArray?
fun keyfileToStore(keyfile: Uri?): ByteArray?
fun hardwareKeyToStore(): ByteArray?
}
fun requestPasswordFocus() {
passwordView.requestFocusFromTouch()
}
// Auto select the password field and open keyboard
fun focusPasswordFieldAndOpenKeyboard() {
passwordView.postDelayed({
passwordView.requestFocusFromTouch()
val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)
saveState.mCredentialStorage = this.mCredentialStorage
return saveState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mCredentialStorage = state.mCredentialStorage ?: CredentialStorage.DEFAULT
}
internal class SavedState : BaseSavedState {
var mCredentialStorage: CredentialStorage? = null
constructor(superState: Parcelable?) : super(superState) {}
private constructor(parcel: Parcel) : super(parcel) {
mCredentialStorage = CredentialStorage.getFromOrdinal(parcel.readInt())
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(mCredentialStorage?.ordinal ?: 0)
}
companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -107,102 +107,20 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <FrameLayout
android:id="@+id/unlock_container"
android:orientation="vertical"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:elevation="4dp" android:elevation="4dp"
android:paddingTop="6dp"
android:paddingLeft="12dp"
android:paddingStart="12dp"
android:paddingRight="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp"
android:background="?android:attr/windowBackground"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<com.kunzisoft.keepass.view.MainCredentialView
<!-- Password Input --> android:id="@+id/activity_password_credentials"
<RelativeLayout
android:id="@+id/password_input_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/password_input_layout"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_password_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/password_checkbox"
android:layout_toEndOf="@+id/password_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:hint="@string/password"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="yes"
android:focusable="true"
android:focusableInTouchMode="true"
android:autofillHints="password"
android:imeOptions="actionDone|flagNoPersonalizedLearning"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>
<!-- File Input -->
<RelativeLayout
android:id="@+id/container_key_file"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content" />
</FrameLayout>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_keyfile_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.kunzisoft.keepass.view.KeyFileSelectionView
android:id="@+id/keyfile_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_toRightOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkox"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</RelativeLayout>
</LinearLayout>
<!-- Only to see previous view elevation -->
<View
android:layout_width="match_parent"
android:layout_height="48dp"
app:layout_constraintTop_toBottomOf="@+id/unlock_container"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/unlock_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:paddingLeft="12dp"
android:paddingStart="12dp"
android:paddingRight="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp"
android:background="?android:attr/windowBackground"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:targetApi="lollipop">
<!-- Password Input -->
<RelativeLayout
android:id="@+id/password_input_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/password_input_layout"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_password_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/password_checkbox"
android:layout_toEndOf="@+id/password_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:hint="@string/password"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="yes"
android:focusable="true"
android:focusableInTouchMode="true"
android:autofillHints="password"
android:imeOptions="actionDone|flagNoPersonalizedLearning"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>
<!-- File Input -->
<RelativeLayout
android:id="@+id/container_key_file"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_keyfile_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.kunzisoft.keepass.view.KeyFileSelectionView
android:id="@+id/keyfile_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_toRightOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkox"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</RelativeLayout>
</LinearLayout>