diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..0d7eaced0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**KeePass DX (please complete the following information):** + - Version: [e.g. 2.5.0.0beta23] + - Build: [e.g. Free] + +**Android (please complete the following information):** + - Device: [e.g. GalaxyS8] + - Version [e.g. 8.1] + - Browser [e.g. Chrome] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..4fe86d5ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/CHANGELOG b/CHANGELOG index 80e506c10..78978fcb2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ KeepassDX (2.5.0.0beta24) - * Add settings + * Add settings (Color, Security, Master Key) * Show history of each entry + * Auto repair database for nodes with same UUID * Fix settings * Fix edit group * Fix small bugs diff --git a/app/build.gradle b/app/build.gradle index fe67efd82..29dded4b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 28 buildToolsVersion '28.0.3' + ndkVersion "20.0.5594570" defaultConfig { applicationId "com.kunzisoft.keepass" @@ -99,6 +100,8 @@ dependencies { implementation "com.madgag.spongycastle:prov:$spongycastleVersion" // Time implementation 'joda-time:joda-time:2.9.9' + // Color + implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3' // Education implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0' // Apache Commons Collections diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b19a964e5..69114e490 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -131,7 +131,8 @@ + android:label="@string/keyboard_name" + android:exported="true"> diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index fd15681be..e187a21c4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -50,6 +50,7 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.EntryContentsView +import java.util.* class EntryActivity : LockingHideActivity() { @@ -86,7 +87,7 @@ class EntryActivity : LockingHideActivity() { // Get Entry from UUID try { - val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY) + val keyEntry: PwNodeId = intent.getParcelableExtra(KEY_ENTRY) mEntry = currentDatabase.getEntryById(keyEntry) } catch (e: ClassCastException) { Log.e(TAG, "Unable to retrieve the entry key") diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index c4051ac92..f9f94712a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -44,6 +44,7 @@ import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.EntryEditContentsView +import java.util.* class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, @@ -90,7 +91,7 @@ class EntryEditActivity : LockingHideActivity(), mDatabase = Database.getInstance() // Entry is retrieve, it's an entry to update - intent.getParcelableExtra>(KEY_ENTRY)?.let { + intent.getParcelableExtra>(KEY_ENTRY)?.let { mIsNew = false // Create an Entry copy to modify from the database entry mEntry = mDatabase?.getEntryById(it) @@ -176,7 +177,7 @@ class EntryEditActivity : LockingHideActivity(), // Set info in view entryEditContentsView?.apply { title = newEntry.title - username = newEntry.username + username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username url = newEntry.url password = newEntry.password notes = newEntry.notes diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 5904e5a3b..69d45b087 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -311,7 +311,7 @@ class FileDatabaseSelectActivity : StylishActivity(), private val keyFileUri: Uri?) : ActionRunnable() { override fun run() { - finishRun(true, null) + finishRun(true) } override fun onFinishRun(result: Result) { @@ -353,7 +353,8 @@ class FileDatabaseSelectActivity : StylishActivity(), if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { mDatabaseFileUri = data?.data if (mDatabaseFileUri != null) { - AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") + AssignMasterKeyDialogFragment.getInstance(true) + .show(supportFragmentManager, "passwordDialog") } // else { // TODO Show error diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 6184bc966..d48ccccb6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -28,7 +28,6 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Handler -import android.util.AttributeSet import android.util.Log import android.view.Menu import android.view.MenuItem @@ -78,6 +77,7 @@ class GroupActivity : LockingActivity(), private var searchTitleView: View? = null private var toolbarPaste: Toolbar? = null private var iconView: ImageView? = null + private var numberChildrenView: TextView? = null private var modeTitleView: TextView? = null private var addNodeButtonView: AddNodeButtonView? = null private var groupNameView: TextView? = null @@ -110,7 +110,8 @@ class GroupActivity : LockingActivity(), setContentView(layoutInflater.inflate(R.layout.activity_group, null)) // Initialize views - iconView = findViewById(R.id.icon) + iconView = findViewById(R.id.group_icon) + numberChildrenView = findViewById(R.id.group_numbers) addNodeButtonView = findViewById(R.id.add_node_button) toolbar = findViewById(R.id.toolbar) searchTitleView = findViewById(R.id.search_title) @@ -361,6 +362,16 @@ class GroupActivity : LockingActivity(), } } + // Assign number of children + numberChildrenView?.apply { + if (PreferencesUtil.showNumberEntries(context)) { + text = mCurrentGroup?.getChildEntries(true)?.size?.toString() ?: "" + visibility = View.VISIBLE + } else { + visibility = View.GONE + } + } + // Show selection mode message if needed if (mSelectionMode) { modeTitleView?.visibility = View.VISIBLE diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt index 3346deb1f..ac6e22cf3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -207,7 +207,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis R.id.menu_sort -> { context?.let { context -> val sortDialogFragment: SortDialogFragment = - if (Database.getInstance().isRecycleBinAvailable + if (Database.getInstance().allowRecycleBin && Database.getInstance().isRecycleBinEnabled) { SortDialogFragment.getInstance( PreferencesUtil.getListSort(context), diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index bfb85ab32..4173f40e6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -44,6 +44,7 @@ import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.widget.* import androidx.biometric.BiometricManager import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper @@ -61,6 +62,8 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager +import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException +import com.kunzisoft.keepass.database.search.SearchDbHelper import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.MenuUtil @@ -69,7 +72,6 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.asError import kotlinx.android.synthetic.main.activity_password.* import java.io.FileNotFoundException -import java.lang.ref.WeakReference class PasswordActivity : StylishActivity() { @@ -87,6 +89,8 @@ class PasswordActivity : StylishActivity() { private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null private var mDatabaseFileUri: Uri? = null + private var mDatabaseKeyFileUri: Uri? = null + private var prefs: SharedPreferences? = null private var mRememberKeyFile: Boolean = false @@ -101,8 +105,7 @@ class PasswordActivity : StylishActivity() { prefs = PreferenceManager.getDefaultSharedPreferences(this) - mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key), - resources.getBoolean(R.bool.keyfile_default)) + mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this) setContentView(R.layout.activity_password) @@ -215,6 +218,7 @@ class PasswordActivity : StylishActivity() { private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) { mDatabaseFileUri = databaseFileUri + mDatabaseKeyFileUri = keyFileUri // Define title databaseFileUri?.let { @@ -236,11 +240,13 @@ class PasswordActivity : StylishActivity() { newDefaultFileName = databaseFileUri ?: newDefaultFileName } - newDefaultFileName?.let { - prefs?.edit()?.apply { + prefs?.edit()?.apply { + newDefaultFileName?.let { putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString()) - apply() + } ?: kotlin.run { + remove(KEY_DEFAULT_DATABASE_PATH) } + apply() } val backupManager = BackupManager(this@PasswordActivity) @@ -384,14 +390,18 @@ class PasswordActivity : StylishActivity() { keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) { val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password - val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile - loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity) + verifyKeyFileCheckbox(keyFile) + loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity) } private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) { val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) - val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile - loadDatabase(password, keyFileUri) + verifyKeyFileCheckbox(keyFile) + loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri) + } + + private fun verifyKeyFileCheckbox(keyFile: Uri?) { + mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile } private fun removePassword() { @@ -399,7 +409,10 @@ class PasswordActivity : StylishActivity() { checkboxPasswordView?.isChecked = false } - private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) { + private fun loadDatabase(databaseFileUri: Uri?, + password: String?, + keyFileUri: Uri?, + cipherDatabaseEntity: CipherDatabaseEntity? = null) { runOnUiThread { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { @@ -411,65 +424,119 @@ class PasswordActivity : StylishActivity() { val database = Database.getInstance() database.closeAndClear(applicationContext.filesDir) - mDatabaseFileUri?.let { databaseUri -> - // Show the progress dialog and load the database - ProgressDialogThread(this, - { progressTaskUpdater -> - LoadDatabaseRunnable( - WeakReference(this@PasswordActivity), - database, - databaseUri, - password, - keyFile, - progressTaskUpdater, - AfterLoadingDatabase(database, password, cipherDatabaseEntity)) - }, - R.string.loading_database).start() - } - } + databaseFileUri?.let { databaseUri -> - /** - * Called after verify and try to opening the database - */ - private inner class AfterLoadingDatabase(val database: Database, val password: String?, - val cipherDatabaseEntity: CipherDatabaseEntity? = null) - : ActionRunnable() { + val onFinishLoadDatabase = object: ActionRunnable() { + override fun onFinishRun(result: Result) { + runOnUiThread { + // Recheck fingerprint if error + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) { + // Stay with the same mode and init it + advancedUnlockedManager?.initBiometricMode() + } + } - override fun onFinishRun(result: Result) { - runOnUiThread { - // Recheck fingerprint if error - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) { - // Stay with the same mode and init it - advancedUnlockedManager?.initBiometricMode() - } - } - - if (result.isSuccess) { - // Remove the password in view in all cases - removePassword() - - // Register the biometric - if (cipherDatabaseEntity != null) { - CipherDatabaseAction.getInstance(this@PasswordActivity) - .addOrUpdateCipherDatabase(cipherDatabaseEntity) { - checkAndLaunchGroupActivity(database, password) + if (result.isSuccess) { + // Save keyFile in app database + if (mRememberKeyFile) { + mDatabaseFileUri?.let { databaseUri -> + saveKeyFileData(databaseUri, mDatabaseKeyFileUri) } - } else { - checkAndLaunchGroupActivity(database, password) - } + } - } else { - if (result.message != null && result.message!!.isNotEmpty()) { - Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show() + // Remove the password in view in all cases + removePassword() + + // Register the biometric + if (cipherDatabaseEntity != null) { + CipherDatabaseAction.getInstance(this@PasswordActivity) + .addOrUpdateCipherDatabase(cipherDatabaseEntity) { + checkAndLaunchGroupActivity(database, password, keyFileUri) + } + } else { + checkAndLaunchGroupActivity(database, password, keyFileUri) + } + + } else { + var resultError = "" + val resultException = result.exception + val resultMessage = result.message + + if (resultException != null) { + resultError = resultException.getLocalizedMessage(resources) + if (resultException is LoadDatabaseDuplicateUuidException) + showLoadDatabaseDuplicateUuidMessage { + showProgressDialogAndLoadDatabase(database, + databaseUri, + password, + keyFileUri, + true, + this) + } + } + + if (resultMessage != null && resultMessage.isNotEmpty()) { + resultError = "$resultError $resultMessage" + } + + Log.e(TAG, resultError, resultException) + + Snackbar.make(activity_password_coordinator_layout, resultError, Snackbar.LENGTH_LONG).asError().show() + } } } } + + // Show the progress dialog and load the database + showProgressDialogAndLoadDatabase(database, + databaseUri, + password, + keyFileUri, + false, + onFinishLoadDatabase) } } - private fun checkAndLaunchGroupActivity(database: Database, password: String?) { - if (database.validatePasswordEncoding(password)) { + private fun showProgressDialogAndLoadDatabase(database: Database, + databaseUri: Uri, + password: String?, + keyFile: Uri?, + fixDuplicateUUID: Boolean, + onFinishLoadDatabase: ActionRunnable) { + ProgressDialogThread(this, + { progressTaskUpdater -> + LoadDatabaseRunnable( + database, + databaseUri, + password, + keyFile, + this@PasswordActivity.contentResolver, + this@PasswordActivity.filesDir, + SearchDbHelper(PreferencesUtil.omitBackup(this@PasswordActivity)), + fixDuplicateUUID, + progressTaskUpdater, + onFinishLoadDatabase) + }, + R.string.loading_database).start() + } + + private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) { + DuplicateUuidDialog().apply { + positiveAction = loadDatabaseWithFix + }.show(supportFragmentManager, "duplicateUUIDDialog") + } + + private fun saveKeyFileData(databaseUri: Uri, keyUri: Uri?) { + var keyFileUri = keyUri + if (!mRememberKeyFile) { + keyFileUri = null + } + FileDatabaseHistoryAction.getInstance(this).addOrUpdateDatabaseUri(databaseUri, keyFileUri) + } + + private fun checkAndLaunchGroupActivity(database: Database, password: String?, keyFileUri: Uri?) { + if (database.validatePasswordEncoding(password, keyFileUri != null)) { launchGroupActivity() } else { PasswordEncodingDialogFragment().apply { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index 8672ab1e0..f78a239ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() { private var rootView: View? = null private var passwordCheckBox: CompoundButton? = null + + private var passwordTextInputLayout: TextInputLayout? = null private var passwordView: TextView? = null private var passwordRepeatTextInputLayout: TextInputLayout? = null private var passwordRepeatView: TextView? = null @@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { activity?.let { activity -> + + var allowNoMasterKey = false + arguments?.apply { + if (containsKey(ALLOW_NO_MASTER_KEY_ARG)) + allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false) + } + val builder = AlertDialog.Builder(activity) val inflater = activity.layoutInflater @@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() { .setTitle(R.string.assign_master_key) // Add action buttons .setPositiveButton(android.R.string.ok) { _, _ -> } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) + passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout) passwordView = rootView?.findViewById(R.id.pass_password) passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password) @@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() { var error = verifyPassword() || verifyFile() if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { error = true - showNoKeyConfirmationDialog() + if (allowNoMasterKey) + showNoKeyConfirmationDialog() + else { + passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials) + } } if (!error) { mListener?.onAssignKeyDialogPositiveClick( @@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { showEmptyPasswordConfirmationDialog() } } + return error } @@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { this@AssignMasterKeyDialogFragment.dismiss() } } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } builder.create().show() } } @@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { keyFileCheckBox!!.isChecked, mKeyFile) this@AssignMasterKeyDialogFragment.dismiss() } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } builder.create().show() } } @@ -255,4 +270,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } } } + + companion object { + + private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG" + + fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment { + val fragment = AssignMasterKeyDialogFragment() + val args = Bundle() + args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey) + fragment.arguments = args + return fragment + } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt index a981e04d0..3dd5514ac 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/BrowserDialogFragment.kt @@ -36,7 +36,7 @@ class BrowserDialogFragment : DialogFragment() { // Get the layout inflater val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null) builder.setView(root) - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } val textDescription = root.findViewById(R.id.file_manager_install_description) textDescription.text = getString(R.string.file_manager_install_description) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt new file mode 100644 index 000000000..219dd9c01 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DuplicateUuidDialog.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX 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. + * + * KeePass DX 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 KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.kunzisoft.keepass.R + +class DuplicateUuidDialog : DialogFragment() { + + var positiveAction: (() -> Unit)? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + // Use the Builder class for convenient dialog construction + val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply { + val message = getString(R.string.contains_duplicate_uuid) + + "\n\n" + getString(R.string.contains_duplicate_uuid_procedure) + setMessage(message) + setPositiveButton(getString(android.R.string.ok)) { _, _ -> + positiveAction?.invoke() + dismiss() + } + setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() } + } + // Create the AlertDialog object and return it + return builder.create() + } + return super.onCreateDialog(savedInstanceState) + } + + override fun onPause() { + super.onPause() + this.dismiss() + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt index 350049fba..156a0be88 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt @@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() { dismiss() } - .setNegativeButton(R.string.cancel) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> val bundle = Bundle() mListener?.cancelPassword(bundle) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt index 032f80cb8..8f126d083 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt @@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP val builder = AlertDialog.Builder(activity) builder.setView(root) .setPositiveButton(android.R.string.ok, null) - .setNegativeButton(R.string.cancel) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> editGroupListener?.cancelEditGroup( editGroupDialogAction, nameTextView?.text?.toString(), diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt index 1e218ccff..7b0691ba9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/IconPickerDialogFragment.kt @@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() { dismiss() } - builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() } + builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() } return builder.create() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt index 87b1baa87..4cb8f1234 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt @@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(activity) builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning) builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener) - builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() } + builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } return builder.create() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt index 7f67091ab..9c17defbb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt @@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() { // Add action buttons .setPositiveButton(android.R.string.ok ) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) } - .setNegativeButton(R.string.cancel) { _, _ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } val ascendingView = rootView.findViewById(R.id.sort_selection_ascending) // Check if is ascending or descending diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt index 2698d32cd..158e260df 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt @@ -249,11 +249,18 @@ class AdvancedUnlockedManager(var context: FragmentActivity, biometricUnlockDatabaseHelper = null } + // Only to fix multiple fingerprint menu #332 + private var addBiometricMenuInProgress = false fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) { - cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { - if ((biometricMode != Mode.UNAVAILABLE - && biometricMode != Mode.NOT_CONFIGURED) && it) - menuInflater.inflate(R.menu.advanced_unlock, menu) + if (!addBiometricMenuInProgress) { + addBiometricMenuInProgress = true + cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { + if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.NOT_CONFIGURED) + && it) { + menuInflater.inflate(R.menu.advanced_unlock, menu) + addBiometricMenuInProgress = false + } + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index a72f1317c..17de99ad7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action import android.content.Context import android.net.Uri import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException +import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.UriUtil import java.io.IOException @@ -63,7 +63,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor( // To save the database super.run() finishRun(true) - } catch (e: InvalidKeyFileException) { + } catch (e: LoadDatabaseInvalidKeyFileException) { erase(mBackupKey) finishRun(false, e.message) } catch (e: IOException) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index 1b31b3274..b164fd70c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -19,118 +19,45 @@ */ package com.kunzisoft.keepass.database.action -import android.content.Context +import android.content.ContentResolver import android.net.Uri -import android.preference.PreferenceManager -import androidx.annotation.StringRes -import android.util.Log -import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.* -import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.search.SearchDbHelper import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater -import java.io.FileNotFoundException -import java.io.IOException -import java.lang.ref.WeakReference +import java.io.File -class LoadDatabaseRunnable(private val mWeakContext: WeakReference, - private val mDatabase: Database, +class LoadDatabaseRunnable(private val mDatabase: Database, private val mUri: Uri, private val mPass: String?, private val mKey: Uri?, + private val contentResolver: ContentResolver, + private val cacheDirectory: File, + private val mSearchHelper: SearchDbHelper, + private val mFixDuplicateUUID: Boolean, private val progressTaskUpdater: ProgressTaskUpdater?, nestedAction: ActionRunnable) : ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) { - private val mRememberKeyFile: Boolean - get() { - return mWeakContext.get()?.let { - PreferenceManager.getDefaultSharedPreferences(it) - .getBoolean(it.getString(R.string.keyfile_key), - it.resources.getBoolean(R.bool.keyfile_default)) - } ?: true - } - override fun run() { try { - mWeakContext.get()?.let { - mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater) - saveFileData(mUri, mKey) - finishRun(true) - } ?: finishRun(false, "Context null") - } catch (e: ArcFourException) { - catchError(e, R.string.error_arc4) - return - } catch (e: InvalidPasswordException) { - catchError(e, R.string.invalid_password) - return - } catch (e: ContentFileNotFoundException) { - catchError(e, R.string.file_not_found_content) - return - } catch (e: FileNotFoundException) { - catchError(e, R.string.file_not_found) - return - } catch (e: IOException) { - var messageId = R.string.error_load_database - e.message?.let { - if (it.contains("Hash failed with code")) - messageId = R.string.error_load_database_KDF_memory - } - catchError(e, messageId, true) - return - } catch (e: KeyFileEmptyException) { - catchError(e, R.string.keyfile_is_empty) - return - } catch (e: InvalidAlgorithmException) { - catchError(e, R.string.invalid_algorithm) - return - } catch (e: InvalidKeyFileException) { - catchError(e, R.string.keyfile_does_not_exist) - return - } catch (e: InvalidDBSignatureException) { - catchError(e, R.string.invalid_db_sig) - return - } catch (e: InvalidDBVersionException) { - catchError(e, R.string.unsupported_db_version) - return - } catch (e: InvalidDBException) { - catchError(e, R.string.error_invalid_db) - return - } catch (e: OutOfMemoryError) { - catchError(e, R.string.error_out_of_memory) - return - } catch (e: Exception) { - catchError(e, R.string.error_load_database, true) - return + mDatabase.loadData(mUri, mPass, mKey, + contentResolver, + cacheDirectory, + mSearchHelper, + mFixDuplicateUUID, + progressTaskUpdater) + finishRun(true) } - } - - private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) { - var errorMessage = mWeakContext.get()?.getString(messageId) - Log.e(TAG, errorMessage, e) - if (addThrowableMessage) - errorMessage = errorMessage + " " + e.localizedMessage - finishRun(false, errorMessage) - } - - private fun saveFileData(uri: Uri, key: Uri?) { - var keyFileUri = key - if (!mRememberKeyFile) { - keyFileUri = null - } - mWeakContext.get()?.let { - FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri) + catch (e: LoadDatabaseException) { + finishRun(false, e) } } override fun onFinishRun(result: Result) { if (!result.isSuccess) { - mDatabase.closeAndClear(mWeakContext.get()?.filesDir) + mDatabase.closeAndClear(cacheDirectory) } } - - companion object { - private val TAG = LoadDatabaseRunnable::class.java.name - } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt index bba0a529f..62aa0956e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/SaveDatabaseRunnable.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.tasks.ActionRunnable import java.io.IOException @@ -37,7 +37,7 @@ abstract class SaveDatabaseRunnable(protected var context: Context, database.saveData(context.contentResolver) } catch (e: IOException) { finishRun(false, e.message) - } catch (e: PwDbOutputException) { + } catch (e: DatabaseOutputException) { finishRun(false, e.message) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index c628ea951..c308e99e6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.database.element import android.content.ContentResolver -import android.content.Context import android.content.res.Resources import android.database.Cursor import android.net.Uri @@ -39,7 +38,6 @@ import com.kunzisoft.keepass.database.file.save.PwDbV3Output import com.kunzisoft.keepass.database.file.save.PwDbV4Output import com.kunzisoft.keepass.database.search.SearchDbHelper import com.kunzisoft.keepass.icons.IconDrawableFactory -import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.stream.LEDataInputStream import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.SingletonHolder @@ -56,7 +54,8 @@ class Database { private var pwDatabaseV4: PwDatabaseV4? = null private var mUri: Uri? = null - private var searchHelper: SearchDbHelper? = null + private var mSearchHelper: SearchDbHelper? = null + var isReadOnly = false val drawFactory = IconDrawableFactory() @@ -68,42 +67,120 @@ class Database { return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory() } - val name: String + val allowName: Boolean + get() = pwDatabaseV4 != null + + var name: String get() { return pwDatabaseV4?.name ?: "" } + set(name) { + pwDatabaseV4?.name = name + pwDatabaseV4?.nameChanged = PwDate() + } - val description: String + val allowDescription: Boolean + get() = pwDatabaseV4 != null + + var description: String get() { return pwDatabaseV4?.description ?: "" } + set(description) { + pwDatabaseV4?.description = description + pwDatabaseV4?.descriptionChanged = PwDate() + } + + val allowDefaultUsername: Boolean + get() = pwDatabaseV4 != null + // TODO get() = pwDatabaseV3 != null || pwDatabaseV4 != null var defaultUsername: String get() { - return pwDatabaseV4?.defaultUserName ?: "" + return pwDatabaseV4?.defaultUserName ?: "" // TODO pwDatabaseV3 default username } set(username) { pwDatabaseV4?.defaultUserName = username pwDatabaseV4?.defaultUserNameChanged = PwDate() } + val allowCustomColor: Boolean + get() = pwDatabaseV4 != null + // TODO get() = pwDatabaseV3 != null || pwDatabaseV4 != null + + // with format "#000000" + var customColor: String + get() { + return pwDatabaseV4?.color ?: "" // TODO pwDatabaseV3 color + } + set(value) { + // TODO Check color string + pwDatabaseV4?.color = value + } + + val version: String + get() = pwDatabaseV3?.version ?: pwDatabaseV4?.version ?: "-" + + val allowDataCompression: Boolean + get() = pwDatabaseV4 != null + val availableCompressionAlgorithms: List get() = pwDatabaseV4?.availableCompressionAlgorithms ?: ArrayList() - val compressionAlgorithm: PwCompressionAlgorithm? + var compressionAlgorithm: PwCompressionAlgorithm? get() = pwDatabaseV4?.compressionAlgorithm + set(value) { + value?.let { + pwDatabaseV4?.compressionAlgorithm = it + } + } + + val allowNoMasterKey: Boolean + get() = pwDatabaseV4 != null + + val allowEncryptionAlgorithmModification: Boolean + get() = availableEncryptionAlgorithms.size > 1 + + fun getEncryptionAlgorithmName(resources: Resources): String { + return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) + ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) + ?: "" + } val availableEncryptionAlgorithms: List get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList() - val encryptionAlgorithm: PwEncryptionAlgorithm? + var encryptionAlgorithm: PwEncryptionAlgorithm? get() = pwDatabaseV3?.encryptionAlgorithm ?: pwDatabaseV4?.encryptionAlgorithm + set(algorithm) { + algorithm?.let { + pwDatabaseV4?.encryptionAlgorithm = algorithm + pwDatabaseV4?.setDataEngine(algorithm.cipherEngine) + pwDatabaseV4?.dataCipher = algorithm.dataCipher + } + } val availableKdfEngines: List get() = pwDatabaseV3?.kdfAvailableList ?: pwDatabaseV4?.kdfAvailableList ?: ArrayList() - val kdfEngine: KdfEngine? + val allowKdfModification: Boolean + get() = availableKdfEngines.size > 1 + + var kdfEngine: KdfEngine? get() = pwDatabaseV3?.kdfEngine ?: pwDatabaseV4?.kdfEngine + set(kdfEngine) { + kdfEngine?.let { + if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid) + pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters + numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds + memoryUsage = kdfEngine.defaultMemoryUsage + parallelism = kdfEngine.defaultParallelism + } + } + + fun getKeyDerivationName(resources: Resources): String { + return kdfEngine?.getName(resources) ?: "" + } var numberKeyEncryptionRounds: Long get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0 @@ -168,7 +245,7 @@ class Database { * Determine if RecycleBin is available or not for this version of database * @return true if RecycleBin available */ - val isRecycleBinAvailable: Boolean + val allowRecycleBin: Boolean get() = pwDatabaseV4 != null val isRecycleBinEnabled: Boolean @@ -209,78 +286,95 @@ class Database { this.mUri = databaseUri } - @Throws(IOException::class, InvalidDBException::class) - fun loadData(ctx: Context, uri: Uri, password: String?, keyfile: Uri?, progressTaskUpdater: ProgressTaskUpdater?) { + @Throws(LoadDatabaseException::class) + fun loadData(uri: Uri, password: String?, keyfile: Uri?, + contentResolver: ContentResolver, + cacheDirectory: File, + searchHelper: SearchDbHelper, + fixDuplicateUUID: Boolean, + progressTaskUpdater: ProgressTaskUpdater?) { - mUri = uri - isReadOnly = false - if (uri.scheme == "file") { - val file = File(uri.path!!) - isReadOnly = !file.canWrite() - } - - // Pass Uris as InputStreams - val inputStream: InputStream? try { - inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri) - } catch (e: Exception) { - Log.e("KPD", "Database::loadData", e) - throw ContentFileNotFoundException.getInstance(uri) - } - // Pass KeyFile Uri as InputStreams - var keyFileInputStream: InputStream? = null - keyfile?.let { + mUri = uri + isReadOnly = false + if (uri.scheme == "file") { + val file = File(uri.path!!) + isReadOnly = !file.canWrite() + } + + // Pass Uris as InputStreams + val inputStream: InputStream? try { - keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile) + inputStream = UriUtil.getUriInputStream(contentResolver, uri) } catch (e: Exception) { Log.e("KPD", "Database::loadData", e) - throw ContentFileNotFoundException.getInstance(keyfile) + throw LoadDatabaseFileNotFoundException() } - } - // Load Data + // Pass KeyFile Uri as InputStreams + var keyFileInputStream: InputStream? = null + keyfile?.let { + try { + keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) + } catch (e: Exception) { + Log.e("KPD", "Database::loadData", e) + throw LoadDatabaseFileNotFoundException() + } + } - val bufferedInputStream = BufferedInputStream(inputStream) - if (!bufferedInputStream.markSupported()) { - throw IOException("Input stream does not support mark.") - } + // Load Data - // We'll end up reading 8 bytes to identify the header. Might as well use two extra. - bufferedInputStream.mark(10) + val bufferedInputStream = BufferedInputStream(inputStream) + if (!bufferedInputStream.markSupported()) { + throw IOException("Input stream does not support mark.") + } - // Get the file directory to save the attachments - val sig1 = LEDataInputStream.readInt(bufferedInputStream) - val sig2 = LEDataInputStream.readInt(bufferedInputStream) + // We'll end up reading 8 bytes to identify the header. Might as well use two extra. + bufferedInputStream.mark(10) - // Return to the start - bufferedInputStream.reset() + // Get the file directory to save the attachments + val sig1 = LEDataInputStream.readInt(bufferedInputStream) + val sig2 = LEDataInputStream.readInt(bufferedInputStream) - when { - // Header of database V3 - PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3() - .openDatabase(bufferedInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + // Return to the start + bufferedInputStream.reset() - // Header of database V4 - PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(ctx.filesDir) - .openDatabase(bufferedInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + when { + // Header of database V3 + PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3() + .openDatabase(bufferedInputStream, + password, + keyFileInputStream, + progressTaskUpdater)) - // Header not recognized - else -> throw InvalidDBSignatureException() - } + // Header of database V4 + PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4( + cacheDirectory, + fixDuplicateUUID) + .openDatabase(bufferedInputStream, + password, + keyFileInputStream, + progressTaskUpdater)) - try { - searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx)) + // Header not recognized + else -> throw LoadDatabaseSignatureException() + } + + this.mSearchHelper = searchHelper loaded = true + + } catch (e: LoadDatabaseException) { + throw e + } catch (e: IOException) { + if (e.message?.contains("Hash failed with code") == true) + throw LoadDatabaseKDFMemoryException(e) + else + throw LoadDatabaseIOException(e) + } catch (e: OutOfMemoryError) { + throw LoadDatabaseNoMemoryException(e) } catch (e: Exception) { - Log.e(TAG, "Load can't be performed with this Database version", e) - loaded = false + throw LoadDatabaseException(e) } } @@ -292,7 +386,7 @@ class Database { @JvmOverloads fun search(str: String, max: Int = Integer.MAX_VALUE): GroupVersioned? { - return searchHelper?.search(this, str, max) + return mSearchHelper?.search(this, str, max) } fun searchEntries(query: String): Cursor? { @@ -343,14 +437,14 @@ class Database { return entry } - @Throws(IOException::class, PwDbOutputException::class) + @Throws(IOException::class, DatabaseOutputException::class) fun saveData(contentResolver: ContentResolver) { mUri?.let { saveData(contentResolver, it) } } - @Throws(IOException::class, PwDbOutputException::class) + @Throws(IOException::class, DatabaseOutputException::class) private fun saveData(contentResolver: ContentResolver, uri: Uri) { val errorMessage = "Failed to store database." @@ -419,72 +513,13 @@ class Database { loaded = false } - fun getVersion(): String { - return pwDatabaseV3?.version ?: pwDatabaseV4?.version ?: "unknown" - } - - fun containsName(): Boolean { - pwDatabaseV4?.let { return true } - return false - } - - fun assignName(name: String) { - pwDatabaseV4?.name = name - pwDatabaseV4?.nameChanged = PwDate() - } - - fun containsDescription(): Boolean { - pwDatabaseV4?.let { return true } - return false - } - - fun assignDescription(description: String) { - pwDatabaseV4?.description = description - pwDatabaseV4?.descriptionChanged = PwDate() - } - - fun assignCompressionAlgorithm(algorithm: PwCompressionAlgorithm) { - pwDatabaseV4?.compressionAlgorithm = algorithm - // TODO Compression - } - - fun allowEncryptionAlgorithmModification(): Boolean { - return availableEncryptionAlgorithms.size > 1 - } - - fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) { - pwDatabaseV4?.encryptionAlgorithm = algorithm - pwDatabaseV4?.setDataEngine(algorithm.cipherEngine) - pwDatabaseV4?.dataCipher = algorithm.dataCipher - } - - fun getEncryptionAlgorithmName(resources: Resources): String { - return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) ?: "" - } - - fun allowKdfModification(): Boolean { - return availableKdfEngines.size > 1 - } - - fun assignKdfEngine(kdfEngine: KdfEngine) { - if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid) - pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters - numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds - memoryUsage = kdfEngine.defaultMemoryUsage - parallelism = kdfEngine.defaultParallelism - } - - fun getKeyDerivationName(resources: Resources): String { - return kdfEngine?.getName(resources) ?: "" - } - - fun validatePasswordEncoding(key: String?): Boolean { - return pwDatabaseV3?.validatePasswordEncoding(key) - ?: pwDatabaseV4?.validatePasswordEncoding(key) + fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { + return pwDatabaseV3?.validatePasswordEncoding(password, containsKeyFile) + ?: pwDatabaseV4?.validatePasswordEncoding(password, containsKeyFile) ?: false } - @Throws(InvalidKeyFileException::class, IOException::class) + @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class) fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { pwDatabaseV3?.retrieveMasterKey(key, keyInputStream) pwDatabaseV4?.retrieveMasterKey(key, keyInputStream) @@ -524,7 +559,7 @@ class Database { return null } - fun getEntryById(id: PwNodeId<*>): EntryVersioned? { + fun getEntryById(id: PwNodeId): EntryVersioned? { pwDatabaseV3?.getEntryById(id)?.let { return EntryVersioned(it) } @@ -535,12 +570,14 @@ class Database { } fun getGroupById(id: PwNodeId<*>): GroupVersioned? { - pwDatabaseV3?.getGroupById(id)?.let { - return GroupVersioned(it) - } - pwDatabaseV4?.getGroupById(id)?.let { - return GroupVersioned(it) - } + if (id is PwNodeIdInt) + pwDatabaseV3?.getGroupById(id)?.let { + return GroupVersioned(it) + } + else if (id is PwNodeIdUUID) + pwDatabaseV4?.getGroupById(id)?.let { + return GroupVersioned(it) + } return null } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt index fa4279802..9d5f77fad 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabase.kt @@ -19,10 +19,10 @@ */ package com.kunzisoft.keepass.database.element -import android.util.Log import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException -import com.kunzisoft.keepass.database.exception.KeyFileEmptyException +import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException +import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException +import com.kunzisoft.keepass.database.exception.LoadDatabaseKeyFileEmptyException import com.kunzisoft.keepass.utils.MemoryUtil import java.io.ByteArrayInputStream @@ -35,7 +35,11 @@ import java.security.NoSuchAlgorithmException import java.util.LinkedHashMap import java.util.UUID -abstract class PwDatabase, Entry : PwEntry> { +abstract class PwDatabase< + GroupId, + Group : PwGroup, + Entry : PwEntry + > { // Algorithm used to encrypt the database protected var algorithm: PwEncryptionAlgorithm? = null @@ -51,8 +55,10 @@ abstract class PwDatabase, Entry : PwEntry, Group>() - private var entryIndexes = LinkedHashMap, Entry>() + var changeDuplicateId = false + + private var groupIndexes = LinkedHashMap, Group>() + private var entryIndexes = LinkedHashMap, Entry>() abstract val version: String @@ -72,15 +78,15 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry throw KeyFileEmptyException() + 0L -> throw LoadDatabaseKeyFileEmptyException() 32L -> return keyData 64L -> try { return hexStringToByteArray(String(keyData)) @@ -161,15 +167,18 @@ abstract class PwDatabase, Entry : PwEntry, Entry : PwEntry, Entry : PwEntry + abstract fun newGroupId(): PwNodeId - abstract fun newEntryId(): PwNodeId<*> + abstract fun newEntryId(): PwNodeId abstract fun createGroup(): Group @@ -216,7 +225,7 @@ abstract class PwDatabase, Entry : PwEntry): Boolean { + fun isGroupIdUsed(id: PwNodeId): Boolean { return groupIndexes.containsKey(id) } @@ -231,14 +240,21 @@ abstract class PwDatabase, Entry : PwEntry): Group? { + fun getGroupById(id: PwNodeId): Group? { return this.groupIndexes[id] } fun addGroupIndex(group: Group) { val groupId = group.nodeId if (groupIndexes.containsKey(groupId)) { - Log.e(TAG, "Error, a group with the same UUID $groupId already exists") + if (changeDuplicateId) { + val newGroupId = newGroupId() + group.nodeId = newGroupId + group.parent?.addChildGroup(group) + this.groupIndexes[newGroupId] = group + } else { + throw LoadDatabaseDuplicateUuidException(Type.GROUP, groupId) + } } else { this.groupIndexes[groupId] = group } @@ -258,7 +274,7 @@ abstract class PwDatabase, Entry : PwEntry): Boolean { + fun isEntryIdUsed(id: PwNodeId): Boolean { return entryIndexes.containsKey(id) } @@ -266,15 +282,21 @@ abstract class PwDatabase, Entry : PwEntry): Entry? { + fun getEntryById(id: PwNodeId): Entry? { return this.entryIndexes[id] } fun addEntryIndex(entry: Entry) { val entryId = entry.nodeId if (entryIndexes.containsKey(entryId)) { - // TODO History - Log.e(TAG, "Error, a group with the same UUID $entryId already exists, change the UUID") + if (changeDuplicateId) { + val newEntryId = newEntryId() + entry.nodeId = newEntryId + entry.parent?.addChildEntry(entry) + this.entryIndexes[newEntryId] = entry + } else { + throw LoadDatabaseDuplicateUuidException(Type.ENTRY, entryId) + } } else { this.entryIndexes[entryId] = entry } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt index 4f80d254d..65502af0c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV3.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException +import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException import com.kunzisoft.keepass.stream.NullOutputStream import java.io.IOException import java.io.InputStream @@ -30,7 +30,7 @@ import java.security.DigestOutputStream import java.security.MessageDigest import java.security.NoSuchAlgorithmException -class PwDatabaseV3 : PwDatabase() { +class PwDatabaseV3 : PwDatabase() { private var numKeyEncRounds: Int = 0 @@ -112,7 +112,7 @@ class PwDatabaseV3 : PwDatabase() { return newId } - @Throws(InvalidKeyFileException::class, IOException::class) + @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class) override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { return if (key != null && keyInputStream != null) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt index 377cf0301..eb6c772cc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/PwDatabaseV4.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.crypto.CryptoUtil import com.kunzisoft.keepass.crypto.engine.AesEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.keyDerivation.* -import com.kunzisoft.keepass.database.exception.InvalidKeyFileException +import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.utils.VariantDictionary import org.w3c.dom.Node @@ -40,7 +40,7 @@ import java.util.* import javax.xml.parsers.DocumentBuilderFactory -class PwDatabaseV4 : PwDatabase { +class PwDatabaseV4 : PwDatabase { var hmacKey: ByteArray? = null private set @@ -236,7 +236,7 @@ class PwDatabaseV4 : PwDatabase { return getCustomData().isNotEmpty() } - @Throws(InvalidKeyFileException::class, IOException::class) + @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class) public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { var masterKey = byteArrayOf() @@ -461,10 +461,10 @@ class PwDatabaseV4 : PwDatabase { return publicCustomData.size() > 0 } - override fun validatePasswordEncoding(key: String?): Boolean { - if (key == null) + override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { + if (password == null) return true - return super.validatePasswordEncoding(key) + return super.validatePasswordEncoding(password, containsKeyFile) } override fun clearCache() { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt deleted file mode 100644 index 9ad91f6ca..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/ArcFourException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class ArcFourException : InvalidDBException() { - companion object { - private const val serialVersionUID = 2103983626687861237L - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt deleted file mode 100644 index 2c9f4c3c9..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/ContentFileNotFoundException.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -import android.net.Uri -import java.io.FileNotFoundException - -class ContentFileNotFoundException : FileNotFoundException() { - companion object { - fun getInstance(uri: Uri?): FileNotFoundException { - if (uri == null) { - return FileNotFoundException() - } - - val scheme = uri.scheme - return if (scheme != null - && scheme.isNotEmpty() - && scheme.equals("content", ignoreCase = true)) { - ContentFileNotFoundException() - } else FileNotFoundException() - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseOutputException.kt similarity index 87% rename from app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt rename to app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseOutputException.kt index d892cdee2..d08e82cf4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/PwDbOutputException.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseOutputException.kt @@ -19,14 +19,10 @@ */ package com.kunzisoft.keepass.database.exception -class PwDbOutputException : Exception { +class DatabaseOutputException : Exception { constructor(string: String) : super(string) constructor(string: String, e: Exception) : super(string, e) constructor(e: Exception) : super(e) - - companion object { - private const val serialVersionUID = 3321212743159473368L - } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt deleted file mode 100644 index 99cc71f2c..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidAlgorithmException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidAlgorithmException : InvalidDBException() { - companion object { - private const val serialVersionUID = 3062682891863487208L - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt deleted file mode 100644 index d50721f39..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBException.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -open class InvalidDBException : Exception { - - constructor(str: String) : super(str) - - constructor() : super() - - companion object { - private const val serialVersionUID = 5191964825154190923L - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt deleted file mode 100644 index 9445755b0..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBSignatureException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidDBSignatureException : InvalidDBException() { - companion object { - private const val serialVersionUID = -5358923878743513758L - } - -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt deleted file mode 100644 index 577bc0052..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidDBVersionException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidDBVersionException : InvalidDBException() { - companion object { - private const val serialVersionUID = -4260650987856400586L - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt deleted file mode 100644 index d7f54fcec..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidKeyFileException.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */package com.kunzisoft.keepass.database.exception - -open class InvalidKeyFileException : InvalidDBException() { - companion object { - private const val serialVersionUID = 5540694419562294464L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt deleted file mode 100644 index dcd51b94d..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/InvalidPasswordException.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class InvalidPasswordException : InvalidDBException() { - companion object { - private const val serialVersionUID = -8729476180242058319L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt deleted file mode 100644 index 474d2dfea..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/KeyFileEmptyException.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePass DX. - * - * KeePass DX 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. - * - * KeePass DX 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 KeePass DX. If not, see . - * - */ -package com.kunzisoft.keepass.database.exception - -class KeyFileEmptyException : InvalidKeyFileException() { - companion object { - private const val serialVersionUID = -1630780661204212325L - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/LoadDatabaseException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/LoadDatabaseException.kt new file mode 100644 index 000000000..4f9e2025a --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/LoadDatabaseException.kt @@ -0,0 +1,75 @@ +package com.kunzisoft.keepass.database.exception + +import android.content.res.Resources +import androidx.annotation.StringRes +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.PwNodeId +import com.kunzisoft.keepass.database.element.Type +import java.io.IOException + + +class LoadDatabaseArcFourException : + LoadDatabaseException(R.string.error_arc4) + +class LoadDatabaseFileNotFoundException : + LoadDatabaseException(R.string.file_not_found_content) + +class LoadDatabaseInvalidAlgorithmException : + LoadDatabaseException(R.string.invalid_algorithm) + +class LoadDatabaseDuplicateUuidException(type: Type, uuid: PwNodeId<*>): + LoadDatabaseException(R.string.invalid_db_same_uuid, type.name, uuid.toString()) + +class LoadDatabaseIOException(exception: IOException) : + LoadDatabaseException(exception, R.string.error_load_database) + +class LoadDatabaseKDFMemoryException(exception: IOException) : + LoadDatabaseException(exception, R.string.error_load_database_KDF_memory) + +class LoadDatabaseSignatureException : + LoadDatabaseException(R.string.invalid_db_sig) + +class LoadDatabaseVersionException : + LoadDatabaseException(R.string.unsupported_db_version) + +open class LoadDatabaseInvalidKeyFileException : + LoadDatabaseException(R.string.keyfile_does_not_exist) + +class LoadDatabaseInvalidPasswordException : + LoadDatabaseException(R.string.invalid_password) + +class LoadDatabaseKeyFileEmptyException : + LoadDatabaseException(R.string.keyfile_is_empty) + +class LoadDatabaseNoMemoryException(exception: OutOfMemoryError) : + LoadDatabaseException(exception, R.string.error_out_of_memory) + +open class LoadDatabaseException : Exception { + + @StringRes + var errorId: Int = R.string.error_load_database + var parameters: (Array)? = null + + constructor(errorMessageId: Int) : super() { + errorId = errorMessageId + } + + constructor(errorMessageId: Int, vararg params: String) : super() { + errorId = errorMessageId + parameters = params + } + + constructor(throwable: Throwable, errorMessageId: Int? = null) : super(throwable) { + errorMessageId?.let { + errorId = it + } + } + + constructor() : super() + + fun getLocalizedMessage(resources: Resources): String { + parameters?.let { + return resources.getString(errorId, *it) + } ?: return resources.getString(errorId) + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt index 8db01ab9c..0c3f13a8d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/SamsungClipboardException.kt @@ -19,9 +19,4 @@ */ package com.kunzisoft.keepass.database.exception -class SamsungClipboardException(e: Exception) : Exception(e) { - companion object { - private const val serialVersionUID = -3168837280393843509L - } - -} +class SamsungClipboardException(e: Exception) : Exception(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt index 2897148c5..435bf4b48 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt @@ -2,8 +2,4 @@ package com.kunzisoft.keepass.database.exception import java.io.IOException -class UnknownKDF : IOException(message) { - companion object { - private const val message = "Unknown key derivation function" - } -} +class UnknownKDF : IOException("Unknown key derivation function") diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt index 9edbfb354..9cff24a2a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/PwDbHeaderV4.kt @@ -25,7 +25,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.database.NodeHandler import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.InvalidDBVersionException +import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException import com.kunzisoft.keepass.stream.CopyInputStream import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.LEDataInputStream @@ -130,9 +130,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { /** Assumes the input stream is at the beginning of the .kdbx file * @param inputStream * @throws IOException - * @throws InvalidDBVersionException + * @throws LoadDatabaseVersionException */ - @Throws(IOException::class, InvalidDBVersionException::class) + @Throws(IOException::class, LoadDatabaseVersionException::class) fun loadFromFile(inputStream: InputStream): HeaderAndHash { val messageDigest: MessageDigest try { @@ -150,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() { val sig2 = littleEndianDataInputStream.readInt() if (!matchesHeader(sig1, sig2)) { - throw InvalidDBVersionException() + throw LoadDatabaseVersionException() } version = littleEndianDataInputStream.readUInt() // Erase previous value if (!validVersion(version)) { - throw InvalidDBVersionException() + throw LoadDatabaseVersionException() } var done = false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt index 1eec9700e..83bd0bb55 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/Importer.kt @@ -20,13 +20,13 @@ package com.kunzisoft.keepass.database.file.load import com.kunzisoft.keepass.database.element.PwDatabase -import com.kunzisoft.keepass.database.exception.InvalidDBException +import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import java.io.IOException import java.io.InputStream -abstract class Importer> { +abstract class Importer> { /** * Load a versioned database file, return contents in a new PwDatabase. @@ -36,9 +36,9 @@ abstract class Importer> { * @return new PwDatabase container. * * @throws IOException on any file error. - * @throws InvalidDBException on database error. + * @throws LoadDatabaseException on database error. */ - @Throws(IOException::class, InvalidDBException::class) + @Throws(IOException::class, LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt index 3771f2d09..c16c24281 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV3.kt @@ -73,7 +73,7 @@ class ImporterV3 : Importer() { private lateinit var mDatabaseToOpen: PwDatabaseV3 - @Throws(IOException::class, InvalidDBException::class) + @Throws(IOException::class, LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, @@ -92,11 +92,11 @@ class ImporterV3 : Importer() { hdr.loadFromFile(filebuf, 0) if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) { - throw InvalidDBSignatureException() + throw LoadDatabaseSignatureException() } if (!hdr.matchesVersion()) { - throw InvalidDBVersionException() + throw LoadDatabaseVersionException() } progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) @@ -109,7 +109,7 @@ class ImporterV3 : Importer() { } else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) { mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish } else { - throw InvalidAlgorithmException() + throw LoadDatabaseInvalidAlgorithmException() } mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong() @@ -152,7 +152,7 @@ class ImporterV3 : Importer() { } catch (e1: IllegalBlockSizeException) { throw IOException("Invalid block size") } catch (e1: BadPaddingException) { - throw InvalidPasswordException() + throw LoadDatabaseInvalidPasswordException() } val md: MessageDigest @@ -171,7 +171,7 @@ class ImporterV3 : Importer() { if (!Arrays.equals(hash, hdr.contentsHash)) { Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)") - throw InvalidPasswordException() + throw LoadDatabaseInvalidPasswordException() } // New manual root because V3 contains multiple root groups (here available with getRootGroups()) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt index d75237721..e10974c59 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/load/ImporterV4.kt @@ -26,9 +26,9 @@ import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.ArcFourException -import com.kunzisoft.keepass.database.exception.InvalidDBException -import com.kunzisoft.keepass.database.exception.InvalidPasswordException +import com.kunzisoft.keepass.database.exception.LoadDatabaseArcFourException +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidPasswordException import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.database.element.security.ProtectedBinary import com.kunzisoft.keepass.database.element.security.ProtectedString @@ -56,7 +56,8 @@ import javax.crypto.Cipher import javax.crypto.NoSuchPaddingException import kotlin.math.min -class ImporterV4(private val streamDir: File) : Importer() { +class ImporterV4(private val streamDir: File, + private val fixDuplicateUUID: Boolean = false) : Importer() { private var randomStream: StreamCipher? = null private lateinit var mDatabase: PwDatabaseV4 @@ -89,7 +90,7 @@ class ImporterV4(private val streamDir: File) : Importer() { private var entryCustomDataKey: String? = null private var entryCustomDataValue: String? = null - @Throws(IOException::class, InvalidDBException::class) + @Throws(IOException::class, LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, @@ -99,6 +100,9 @@ class ImporterV4(private val streamDir: File) : Importer() { progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) mDatabase = PwDatabaseV4() + + mDatabase.changeDuplicateId = fixDuplicateUUID + val header = PwDbHeaderV4(mDatabase) val headerAndHash = header.loadFromFile(databaseInputStream) @@ -138,14 +142,14 @@ class ImporterV4(private val streamDir: File) : Importer() { try { storedStartBytes = dataDecrypted.readBytes(32) if (storedStartBytes == null || storedStartBytes.size != 32) { - throw InvalidPasswordException() + throw LoadDatabaseInvalidPasswordException() } } catch (e: IOException) { - throw InvalidPasswordException() + throw LoadDatabaseInvalidPasswordException() } if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) { - throw InvalidPasswordException() + throw LoadDatabaseInvalidPasswordException() } isPlain = HashedBlockInputStream(dataDecrypted) @@ -153,18 +157,18 @@ class ImporterV4(private val streamDir: File) : Importer() { val isData = LEDataInputStream(databaseInputStream) val storedHash = isData.readBytes(32) if (!Arrays.equals(storedHash, hashOfHeader)) { - throw InvalidDBException() + throw LoadDatabaseException() } - val hmacKey = mDatabase.hmacKey ?: throw InvalidDBException() + val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException() val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey) val storedHmac = isData.readBytes(32) if (storedHmac == null || storedHmac.size != 32) { - throw InvalidDBException() + throw LoadDatabaseException() } // Mac doesn't match if (!Arrays.equals(headerHmac, storedHmac)) { - throw InvalidDBException() + throw LoadDatabaseException() } val hmIs = HmacBlockInputStream(isData, true, hmacKey) @@ -185,7 +189,7 @@ class ImporterV4(private val streamDir: File) : Importer() { randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) if (randomStream == null) { - throw ArcFourException() + throw LoadDatabaseArcFourException() } readXmlStreamed(isXml) @@ -270,7 +274,7 @@ class ImporterV4(private val streamDir: File) : Importer() { Binaries } - @Throws(IOException::class, InvalidDBException::class) + @Throws(IOException::class, LoadDatabaseException::class) private fun readXmlStreamed(readerStream: InputStream) { try { readDocumentStreamed(createPullParser(readerStream)) @@ -281,7 +285,7 @@ class ImporterV4(private val streamDir: File) : Importer() { } - @Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class) + @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) private fun readDocumentStreamed(xpp: XmlPullParser) { ctxGroups.clear() @@ -312,7 +316,7 @@ class ImporterV4(private val streamDir: File) : Importer() { if (ctxGroups.size != 0) throw IOException("Malformed") } - @Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class) + @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext { val name = xpp.name when (ctx) { @@ -336,7 +340,7 @@ class ImporterV4(private val streamDir: File) : Importer() { if (encodedHash.isNotEmpty() && hashOfHeader != null) { val hash = Base64Coder.decode(encodedHash) if (!Arrays.equals(hash, hashOfHeader)) { - throw InvalidDBException() + throw LoadDatabaseException() } } } else if (name.equals(PwDatabaseV4XML.ElemSettingsChanged, ignoreCase = true)) { @@ -354,7 +358,6 @@ class ImporterV4(private val streamDir: File) : Importer() { } else if (name.equals(PwDatabaseV4XML.ElemDbDefaultUserChanged, ignoreCase = true)) { mDatabase.defaultUserNameChanged = readPwTime(xpp) } else if (name.equals(PwDatabaseV4XML.ElemDbColor, ignoreCase = true)) { - // TODO: Add support to interpret the color if we want to allow changing the database color mDatabase.color = readString(xpp) } else if (name.equals(PwDatabaseV4XML.ElemDbMntncHistoryDays, ignoreCase = true)) { mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt index 3ebadb9f5..6a5720825 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbHeaderOutputV4.kt @@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.database.element.PwDatabaseV4 import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeaderV4 -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.LEDataOutputStream import com.kunzisoft.keepass.stream.MacOutputStream @@ -41,7 +41,7 @@ import java.security.NoSuchAlgorithmException import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec -class PwDbHeaderOutputV4 @Throws(PwDbOutputException::class) +class PwDbHeaderOutputV4 @Throws(DatabaseOutputException::class) constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: OutputStream) : PwDbHeaderOutput() { private val los: LEDataOutputStream private val mos: MacOutputStream @@ -54,13 +54,13 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: try { md = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("SHA-256 not implemented here.") + throw DatabaseOutputException("SHA-256 not implemented here.") } try { db.makeFinalKey(header.masterSeed) } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } val hmac: Mac @@ -69,9 +69,9 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, Types.ULONG_MAX_VALUE), "HmacSHA256") hmac.init(signingKey) } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } catch (e: InvalidKeyException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } dos = DigestOutputStream(os, md) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt index e9a486af6..a670182ca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbOutput.kt @@ -20,7 +20,7 @@ package com.kunzisoft.keepass.database.file.save import com.kunzisoft.keepass.database.file.PwDbHeader -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import java.io.OutputStream import java.security.NoSuchAlgorithmException @@ -28,13 +28,13 @@ import java.security.SecureRandom abstract class PwDbOutput
protected constructor(protected var mOS: OutputStream) { - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) protected open fun setIVs(header: Header): SecureRandom { val random: SecureRandom try { random = SecureRandom.getInstance("SHA1PRNG") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("Does not support secure random number generation.") + throw DatabaseOutputException("Does not support secure random number generation.") } random.nextBytes(header.encryptionIV) @@ -43,10 +43,10 @@ abstract class PwDbOutput
protected constructor(protected v return random } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) abstract fun output() - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) abstract fun outputHeader(outputStream: OutputStream): Header } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt index 8a546c298..5a2710cbe 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV3Output.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.file.save import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeaderV3 import com.kunzisoft.keepass.stream.LEDataOutputStream @@ -42,18 +42,18 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw private var headerHashBlock: ByteArray? = null - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) fun getFinalKey(header: PwDbHeader): ByteArray? { try { val h3 = header as PwDbHeaderV3 mDatabaseV3.makeFinalKey(h3.masterSeed, h3.transformSeed, mDatabaseV3.numberKeyEncryptionRounds) return mDatabaseV3.finalKey } catch (e: IOException) { - throw PwDbOutputException("Key creation failed.", e) + throw DatabaseOutputException("Key creation failed.", e) } } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun output() { // Before we output the header, we should sort our list of groups // and remove any orphaned nodes that are no longer part of the tree hierarchy @@ -74,7 +74,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw throw Exception() } } catch (e: Exception) { - throw PwDbOutputException("Algorithm not supported.", e) + throw DatabaseOutputException("Algorithm not supported.", e) } try { @@ -86,23 +86,23 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw bos.close() } catch (e: InvalidKeyException) { - throw PwDbOutputException("Invalid key", e) + throw DatabaseOutputException("Invalid key", e) } catch (e: InvalidAlgorithmParameterException) { - throw PwDbOutputException("Invalid algorithm parameter.", e) + throw DatabaseOutputException("Invalid algorithm parameter.", e) } catch (e: IOException) { - throw PwDbOutputException("Failed to output final encrypted part.", e) + throw DatabaseOutputException("Failed to output final encrypted part.", e) } } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun setIVs(header: PwDbHeaderV3): SecureRandom { val random = super.setIVs(header) random.nextBytes(header.transformSeed) return random } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun outputHeader(outputStream: OutputStream): PwDbHeaderV3 { // Build header val header = PwDbHeaderV3() @@ -115,7 +115,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw } else if (mDatabaseV3.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) { header.flags = header.flags or PwDbHeaderV3.FLAG_TWOFISH } else { - throw PwDbOutputException("Unsupported algorithm.") + throw DatabaseOutputException("Unsupported algorithm.") } header.version = PwDbHeaderV3.DBVER_DW @@ -130,7 +130,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { messageDigest = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("SHA-256 not implemented here.", e) + throw DatabaseOutputException("SHA-256 not implemented here.", e) } // Header checksum @@ -138,7 +138,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { headerDigest = MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("SHA-256 not implemented here.", e) + throw DatabaseOutputException("SHA-256 not implemented here.", e) } var nos = NullOutputStream() @@ -151,7 +151,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw pho.outputEnd() headerDos.flush() } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } val headerHash = headerDigest.digest() @@ -166,7 +166,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw bos.flush() bos.close() } catch (e: IOException) { - throw PwDbOutputException("Failed to generate checksum.", e) + throw DatabaseOutputException("Failed to generate checksum.", e) } header.contentsHash = messageDigest!!.digest() @@ -181,14 +181,14 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw pho.outputEnd() dos.flush() } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } return header } @Suppress("CAST_NEVER_SUCCEEDS") - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) fun outputPlanGroupAndEntries(os: OutputStream) { val los = LEDataOutputStream(os) @@ -199,7 +199,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw los.writeInt(headerHashBlock!!.size) los.write(headerHashBlock!!) } catch (e: IOException) { - throw PwDbOutputException("Failed to output header hash.", e) + throw DatabaseOutputException("Failed to output header hash.", e) } } @@ -209,7 +209,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { pgo.output() } catch (e: IOException) { - throw PwDbOutputException("Failed to output a tree", e) + throw DatabaseOutputException("Failed to output a tree", e) } } mDatabaseV3.doForEachEntryInIndex { entry -> @@ -217,7 +217,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw try { peo.output() } catch (e: IOException) { - throw PwDbOutputException("Failed to output an entry.", e) + throw DatabaseOutputException("Failed to output an entry.", e) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt index bb662366d..c2a426966 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/save/PwDbV4Output.kt @@ -29,7 +29,7 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.database.* import com.kunzisoft.keepass.database.element.* -import com.kunzisoft.keepass.database.exception.PwDbOutputException +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm import com.kunzisoft.keepass.database.file.PwDbHeaderV4 @@ -63,14 +63,14 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt private var headerHmac: ByteArray? = null private var engine: CipherEngine? = null - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun output() { try { try { engine = CipherFactory.getInstance(mDatabaseV4.dataCipher) } catch (e: NoSuchAlgorithmException) { - throw PwDbOutputException("No such cipher", e) + throw DatabaseOutputException("No such cipher", e) } header = outputHeader(mOS) @@ -104,13 +104,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt outputDatabase(osXml) osXml.close() } catch (e: IllegalArgumentException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } catch (e: IllegalStateException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } } catch (e: IOException) { - throw PwDbOutputException(e) + throw DatabaseOutputException(e) } } @@ -228,7 +228,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt xml.endTag(null, PwDatabaseV4XML.ElemMeta) } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) private fun attachStreamEncryptor(header: PwDbHeaderV4, os: OutputStream): CipherOutputStream { val cipher: Cipher try { @@ -236,13 +236,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseV4.finalKey!!, header.encryptionIV) } catch (e: Exception) { - throw PwDbOutputException("Invalid algorithm.", e) + throw DatabaseOutputException("Invalid algorithm.", e) } return CipherOutputStream(os, cipher) } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun setIVs(header: PwDbHeaderV4): SecureRandom { val random = super.setIVs(header) random.nextBytes(header.masterSeed) @@ -275,7 +275,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) if (randomStream == null) { - throw PwDbOutputException("Invalid random cipher") + throw DatabaseOutputException("Invalid random cipher") } if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) { @@ -285,7 +285,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt return random } - @Throws(PwDbOutputException::class) + @Throws(DatabaseOutputException::class) override fun outputHeader(outputStream: OutputStream): PwDbHeaderV4 { val header = PwDbHeaderV4(mDatabaseV4) @@ -295,7 +295,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt try { pho.output() } catch (e: IOException) { - throw PwDbOutputException("Failed to output the header.", e) + throw DatabaseOutputException("Failed to output the header.", e) } hashOfHeader = pho.hashOfHeader diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt index aeb2fe35e..04390d4e5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt @@ -22,18 +22,23 @@ package com.kunzisoft.keepass.settings import android.content.ActivityNotFoundException import android.content.Intent import android.content.res.Resources +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings -import androidx.annotation.RequiresApi -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.autofill.AutofillManager import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog import androidx.biometric.BiometricManager +import androidx.fragment.app.DialogFragment import androidx.preference.* +import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* @@ -41,12 +46,13 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm +import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.icons.IconPackChooser import com.kunzisoft.keepass.settings.preference.* +import com.kunzisoft.keepass.settings.preference.DialogColorPreference.Companion.DISABLE_COLOR import com.kunzisoft.keepass.settings.preferencedialogfragment.* class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { @@ -56,6 +62,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen private var mCount = 0 + private var dbCustomColorPref: DialogColorPreference? = null private var mRoundPref: InputKdfNumberPreference? = null private var mMemoryPref: InputKdfNumberPreference? = null private var mParallelismPref: InputKdfNumberPreference? = null @@ -339,11 +346,11 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen if (mDatabase.loaded) { - val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_general_key)) + val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_general_key)) - // Db name + // Database name val dbNamePref: InputTextPreference? = findPreference(getString(R.string.database_name_key)) - if (mDatabase.containsName()) { + if (mDatabase.allowName) { dbNamePref?.summary = mDatabase.name } else { dbGeneralPrefCategory?.removePreference(dbNamePref) @@ -351,32 +358,66 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen // Database description val dbDescriptionPref: InputTextPreference? = findPreference(getString(R.string.database_description_key)) - if (mDatabase.containsDescription()) { + if (mDatabase.allowDescription) { dbDescriptionPref?.summary = mDatabase.description } else { dbGeneralPrefCategory?.removePreference(dbDescriptionPref) } - // Database compression - findPreference(getString(R.string.database_data_compression_key)) - ?.summary = (mDatabase.compressionAlgorithm ?: PwCompressionAlgorithm.None).getName(resources) - - // Recycle bin - val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key)) - // TODO Recycle - dbGeneralPrefCategory?.removePreference(recycleBinPref) // To delete - if (mDatabase.isRecycleBinAvailable) { - recycleBinPref?.isChecked = mDatabase.isRecycleBinEnabled - recycleBinPref?.isEnabled = false + // Database default username + val dbDefaultUsername: InputTextPreference? = findPreference(getString(R.string.database_default_username_key)) + if (mDatabase.allowDefaultUsername) { + dbDefaultUsername?.summary = mDatabase.defaultUsername } else { - dbGeneralPrefCategory?.removePreference(recycleBinPref) + dbDefaultUsername?.isEnabled = false + // TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername) + } + + // Database custom color + dbCustomColorPref = findPreference(getString(R.string.database_custom_color_key)) + if (mDatabase.allowCustomColor) { + dbCustomColorPref?.apply { + try { + color = Color.parseColor(mDatabase.customColor) + summary = mDatabase.customColor + } catch (e: Exception) { + color = DISABLE_COLOR + summary = "" + } + } + } else { + dbCustomColorPref?.isEnabled = false + // TODO dbGeneralPrefCategory?.removePreference(dbCustomColorPref) } // Version findPreference(getString(R.string.database_version_key)) - ?.summary = mDatabase.getVersion() + ?.summary = mDatabase.version - findPreference(getString(R.string.database_history_key)) + val dbCompressionPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_compression_key)) + + // Database compression + val databaseDataCompressionPref = findPreference(getString(R.string.database_data_compression_key)) + if (mDatabase.allowDataCompression) { + databaseDataCompressionPref?.summary = (mDatabase.compressionAlgorithm + ?: PwCompressionAlgorithm.None).getName(resources) + } else { + dbCompressionPrefCategory?.isVisible = false + } + + val dbRecycleBinPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_category_recycle_bin_key)) + + // Recycle bin + val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key)) + if (mDatabase.allowRecycleBin) { + recycleBinPref?.isChecked = mDatabase.isRecycleBinEnabled + // TODO Recycle Bin + recycleBinPref?.isEnabled = false + } else { + dbRecycleBinPrefCategory?.isVisible = false + } + + findPreference(getString(R.string.database_category_history_key)) ?.isVisible = mDatabase.manageHistory == true // Max history items @@ -427,7 +468,8 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen findPreference(getString(R.string.settings_database_change_credentials_key))?.apply { onPreferenceClickListener = Preference.OnPreferenceClickListener { fragmentManager?.let { fragmentManager -> - AssignMasterKeyDialogFragment().show(fragmentManager, "passwordDialog") + AssignMasterKeyDialogFragment.getInstance(mDatabase.allowNoMasterKey) + .show(fragmentManager, "passwordDialog") } false } @@ -488,6 +530,27 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen } } + private val colorSelectedListener: ((Boolean, Int)-> Unit)? = { enable, color -> + dbCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false) + if (enable) { + dbCustomColorPref?.color = color + } else { + dbCustomColorPref?.color = DISABLE_COLOR + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + + try { + // To reassign color listener after orientation change + val chromaDialog = fragmentManager?.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat? + chromaDialog?.onColorSelectedListener = colorSelectedListener + } catch (e: Exception) {} + + return view + } + override fun onDisplayPreferenceDialog(preference: Preference?) { var otherDialogFragment = false @@ -502,6 +565,14 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen preference.key == getString(R.string.database_description_key) -> { dialogFragment = DatabaseDescriptionPreferenceDialogFragmentCompat.newInstance(preference.key) } + preference.key == getString(R.string.database_default_username_key) -> { + dialogFragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat.newInstance(preference.key) + } + preference.key == getString(R.string.database_custom_color_key) -> { + dialogFragment = DatabaseColorPreferenceDialogFragmentCompat.newInstance(preference.key).apply { + onColorSelectedListener = colorSelectedListener + } + } preference.key == getString(R.string.database_data_compression_key) -> { dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key) } @@ -536,7 +607,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen if (dialogFragment != null && !mDatabaseReadOnly) { dialogFragment.setTargetFragment(this, 0) - dialogFragment.show(fragmentManager, null) + dialogFragment.show(fragmentManager, TAG_PREF_FRAGMENT) } // Could not be handled here. Try with the super method. else if (otherDialogFragment) { @@ -560,6 +631,8 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen private const val TAG_KEY = "NESTED_KEY" + private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT" + private const val REQUEST_CODE_AUTOFILL = 5201 @JvmOverloads diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index 93c7e7323..46222da3b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -33,6 +33,12 @@ object PreferencesUtil { return prefs.getBoolean(context.getString(R.string.show_read_only_warning), true) } + fun rememberKeyFiles(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.keyfile_key), + context.resources.getBoolean(R.bool.keyfile_default)) + } + fun omitBackup(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.omitbackup_key), diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt index 5f8897212..e28b073b9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -115,7 +115,7 @@ open class SettingsActivity true) } // Show the progress dialog now or after dialog confirmation - if (database.validatePasswordEncoding(masterPassword)) { + if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) { progressDialogThread.start() } else { PasswordEncodingDialogFragment().apply { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt new file mode 100644 index 000000000..a94fc8912 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt @@ -0,0 +1,33 @@ +package com.kunzisoft.keepass.settings.preference + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import androidx.annotation.ColorInt +import com.kunzisoft.androidclearchroma.ChromaPreferenceCompat + +import com.kunzisoft.keepass.R + +class DialogColorPreference @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleRes: Int = defStyleAttr) + : ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) { + + override fun setSummary(summary: CharSequence?) { + if (color == DISABLE_COLOR) + super.setSummary("") + else + super.setSummary(summary) + } + + override fun getDialogLayoutResource(): Int { + return R.layout.pref_dialog_input_color + } + + companion object { + + @ColorInt + const val DISABLE_COLOR: Int = Color.TRANSPARENT + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..1c737f114 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseColorPreferenceDialogFragmentCompat.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX 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. + * + * KeePass DX 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 KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.app.Dialog +import android.graphics.Color +import android.os.Bundle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.CompoundButton +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import com.kunzisoft.androidclearchroma.ChromaUtil +import com.kunzisoft.androidclearchroma.IndicatorMode +import com.kunzisoft.androidclearchroma.colormode.ColorMode +import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment +import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.* +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.tasks.ActionRunnable +import java.lang.Exception + +class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + + private lateinit var rootView: View + private lateinit var enableSwitchView: CompoundButton + private var chromaColorFragment: ChromaColorFragment? = null + + var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + + val alertDialogBuilder = AlertDialog.Builder(activity!!) + + rootView = activity!!.layoutInflater.inflate(R.layout.pref_dialog_input_color, null) + enableSwitchView = rootView.findViewById(R.id.switch_element) + + val fragmentManager = childFragmentManager + chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment? + val fragmentTransaction = fragmentManager.beginTransaction() + + database?.let { database -> + val initColor = try { + enableSwitchView.isChecked = true + Color.parseColor(database.customColor) + } catch (e: Exception) { + enableSwitchView.isChecked = false + DEFAULT_COLOR + } + arguments?.putInt(ARG_INITIAL_COLOR, initColor) + } + + if (chromaColorFragment == null) { + chromaColorFragment = newInstance(arguments) + fragmentTransaction.add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS).commit() + } + + alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ -> + val currentColor = chromaColorFragment!!.currentColor + val customColorEnable = enableSwitchView.isChecked + + onColorSelectedListener?.invoke(customColorEnable, currentColor) + + database?.let { database -> + val newColor = if (customColorEnable) { + ChromaUtil.getFormattedColorString(currentColor, false) + } else { + "" + } + val oldColor = database.customColor + database.customColor = newColor + + actionInUIThreadAfterSaveDatabase = AfterColorSave(newColor, oldColor) + } + + super.onDialogClosed(true) + dismiss() + } + + alertDialogBuilder.setNegativeButton(android.R.string.cancel) { _, _ -> + super.onDialogClosed(false) + dismiss() + } + + alertDialogBuilder.setView(rootView) + + val dialog = alertDialogBuilder.create() + // request a window without the title + dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) + + dialog.setOnShowListener { measureLayout(it as Dialog) } + + return dialog + } + + /** + * Set new dimensions to dialog + * @param ad dialog + */ + private fun measureLayout(ad: Dialog) { + val typedValue = TypedValue() + resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true) + val heightMultiplier = typedValue.float + val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt() + + resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true) + val widthMultiplier = typedValue.float + val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt() + + ad.window?.setLayout(width, height) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return rootView + } + + private inner class AfterColorSave(private val mNewColor: String, + private val mOldColor: String) + : ActionRunnable() { + + override fun onFinishRun(result: Result) { + val defaultColorToShow = + if (result.isSuccess) { + mNewColor + } else { + database?.customColor = mOldColor + mOldColor + } + preference.summary = defaultColorToShow + } + } + + companion object { + private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS" + + @ColorInt + const val DEFAULT_COLOR: Int = Color.WHITE + + fun newInstance(key: String): DatabaseColorPreferenceDialogFragmentCompat { + val fragment = DatabaseColorPreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK) + bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal) + bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt index f68df11ee..fbee376de 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDataCompressionPreferenceDialogFragmentCompat.kt @@ -62,9 +62,7 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat if (compressionSelected != null) { val newAlgorithm = compressionSelected val oldAlgorithm = database.compressionAlgorithm - newAlgorithm?.let { - database.assignCompressionAlgorithm(it) - } + database.compressionAlgorithm = newAlgorithm if (oldAlgorithm != null && newAlgorithm != null) actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm) @@ -88,7 +86,7 @@ class DatabaseDataCompressionPreferenceDialogFragmentCompat if (result.isSuccess) { mNewAlgorithm } else { - database?.assignCompressionAlgorithm(mOldAlgorithm) + database?.compressionAlgorithm = mOldAlgorithm mOldAlgorithm } preference.summary = algorithmToShow.getName(settingsResources) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt new file mode 100644 index 000000000..6c7cb52ff --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDefaultUsernamePreferenceDialogFragmentCompat.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePass DX. + * + * KeePass DX 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. + * + * KeePass DX 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 KeePass DX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preferencedialogfragment + +import android.os.Bundle +import android.view.View +import com.kunzisoft.keepass.tasks.ActionRunnable + +class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + + override fun onBindDialogView(view: View) { + super.onBindDialogView(view) + + inputText = database?.defaultUsername?: "" + } + + override fun onDialogClosed(positiveResult: Boolean) { + database?.let { database -> + if (positiveResult) { + val newDefaultUsername = inputText + val oldDefaultUsername = database.defaultUsername + database.defaultUsername = newDefaultUsername + + actionInUIThreadAfterSaveDatabase = AfterDefaultUsernameSave(newDefaultUsername, oldDefaultUsername) + } + } + + super.onDialogClosed(positiveResult) + } + + private inner class AfterDefaultUsernameSave(private val mNewDefaultUsername: String, + private val mOldDefaultUsername: String) + : ActionRunnable() { + + override fun onFinishRun(result: Result) { + val defaultUsernameToShow = + if (result.isSuccess) { + mNewDefaultUsername + } else { + database?.defaultUsername = mOldDefaultUsername + mOldDefaultUsername + } + preference.summary = defaultUsernameToShow + } + } + + companion object { + + fun newInstance(key: String): DatabaseDefaultUsernamePreferenceDialogFragmentCompat { + val fragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat() + val bundle = Bundle(1) + bundle.putString(ARG_KEY, key) + fragment.arguments = bundle + + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt index 54b4485cd..e08bdb851 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseDescriptionPreferenceDialogFragmentCompat.kt @@ -32,12 +32,14 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreference } override fun onDialogClosed(positiveResult: Boolean) { - if (database != null && positiveResult) { - val newDescription = inputText - val oldDescription = database!!.description - database?.assignDescription(newDescription) + database?.let { database -> + if (positiveResult) { + val newDescription = inputText + val oldDescription = database.description + database.description = newDescription - actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription) + actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription) + } } super.onDialogClosed(positiveResult) @@ -52,7 +54,7 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreference if (result.isSuccess) { mNewDescription } else { - database?.assignDescription(mOldDescription) + database?.description = mOldDescription mOldDescription } preference.summary = descriptionToShow diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt index 825c84bd1..38aef811e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt @@ -59,13 +59,11 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat if (positiveResult) { database?.let { database -> - if (database.allowEncryptionAlgorithmModification()) { + if (database.allowEncryptionAlgorithmModification) { if (algorithmSelected != null) { val newAlgorithm = algorithmSelected val oldAlgorithm = database.encryptionAlgorithm - newAlgorithm?.let { - database.assignEncryptionAlgorithm(it) - } + database.encryptionAlgorithm = newAlgorithm if (oldAlgorithm != null && newAlgorithm != null) actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm) @@ -90,7 +88,7 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat if (result.isSuccess) { mNewAlgorithm } else { - database?.assignEncryptionAlgorithm(mOldAlgorithm) + database?.encryptionAlgorithm = mOldAlgorithm mOldAlgorithm } preference.summary = algorithmToShow.getName(settingsResources) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt index b7ba2f7f5..6b05c14a7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseKeyDerivationPreferenceDialogFragmentCompat.kt @@ -62,11 +62,11 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat override fun onDialogClosed(positiveResult: Boolean) { if (positiveResult) { database?.let { database -> - if (database.allowKdfModification()) { + if (database.allowKdfModification) { val newKdfEngine = kdfEngineSelected val oldKdfEngine = database.kdfEngine if (newKdfEngine != null && oldKdfEngine != null) { - database.assignKdfEngine(newKdfEngine) + database.kdfEngine = newKdfEngine actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newKdfEngine, oldKdfEngine) } } @@ -101,7 +101,7 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat if (result.isSuccess) { mNewKdfEngine } else { - database?.assignKdfEngine(mOldKdfEngine) + database?.kdfEngine = mOldKdfEngine mOldKdfEngine } preference.summary = kdfEngineToShow.getName(settingsResources) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt index a9a3603fe..8b4372037 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseNamePreferenceDialogFragmentCompat.kt @@ -36,7 +36,7 @@ class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogF database?.let { database -> val newName = inputText val oldName = database.name - database.assignName(newName) + database.name = newName actionInUIThreadAfterSaveDatabase = AfterNameSave(newName, oldName) } @@ -54,7 +54,7 @@ class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogF if (result.isSuccess) { mNewName } else { - database?.assignName(mOldName) + database?.name = mOldName mOldName } preference.summary = nameToShow diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt index 0f6fe8888..913b4d018 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseSavePreferenceDialogFragmentCompat.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.content.res.Resources +import android.os.Bundle import android.util.Log import android.view.View import android.widget.Toast @@ -36,10 +37,14 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo protected lateinit var settingsResources: Resources - override fun onBindDialogView(view: View) { - super.onBindDialogView(view) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) this.database = Database.getInstance() + } + + override fun onBindDialogView(view: View) { + super.onBindDialogView(view) activity?.resources?.let { settingsResources = it } } diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt index d75e1ac1e..ad181fa51 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ActionRunnable.kt @@ -24,6 +24,7 @@ import android.content.Context import android.os.Bundle import android.util.Log import android.widget.Toast +import com.kunzisoft.keepass.database.exception.LoadDatabaseException /** * Callback after a task is completed. @@ -52,8 +53,21 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable? * launch the nested action runnable if exists and finish, * else directly finish */ - protected fun finishRun(isSuccess: Boolean, message: String? = null) { + protected fun finishRun(isSuccess: Boolean, + message: String? = null) { + finishRun(isSuccess, null, message) + } + + /** + * If [success] or [executeNestedActionIfResultFalse] true, + * launch the nested action runnable if exists and finish, + * else directly finish + */ + protected fun finishRun(isSuccess: Boolean, + exception: LoadDatabaseException?, + message: String? = null) { result.isSuccess = isSuccess + result.exception = exception result.message = message if (isSuccess || executeNestedActionIfResultFalse) { execute() @@ -89,5 +103,8 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable? /** * Class to manage result from ActionRunnable */ - data class Result(var isSuccess: Boolean = true, var message: String? = null, var data: Bundle? = null) + data class Result(var isSuccess: Boolean = true, + var message: String? = null, + var exception: LoadDatabaseException? = null, + var data: Bundle? = null) } diff --git a/app/src/main/res/layout/activity_group.xml b/app/src/main/res/layout/activity_group.xml index 25d6d43d1..eeace1184 100644 --- a/app/src/main/res/layout/activity_group.xml +++ b/app/src/main/res/layout/activity_group.xml @@ -72,22 +72,39 @@ - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pref_dialog_input_numbers.xml b/app/src/main/res/layout/pref_dialog_input_numbers.xml index 3a961e91d..5082fd234 100644 --- a/app/src/main/res/layout/pref_dialog_input_numbers.xml +++ b/app/src/main/res/layout/pref_dialog_input_numbers.xml @@ -38,7 +38,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" style="@style/KeepassDXStyle.TextAppearance.SmallTitle"/> - - \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index bce2000a5..bf891c822 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -27,7 +27,6 @@ لا تظهر مرة أخرى أقواس تمديد ASCII - إلغاء السماح مُسِحت الحافظة خطأ في الحافظة @@ -54,7 +53,6 @@ اكتب عنوانًا. اسم الحقل قيمة الحقل - تعذر إيجاد الملف. توليد كلمة سر تأكيد كلمة السر اسم المجموعة diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index e4b3f44ee..98deec432 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -34,7 +34,6 @@ Paràmetres de l\'aplicació Parèntesis L\'exploració d\'arxius necessita l\'aplicació Open Intents File Manager, clica a sota per instal·lar-la. Degut a peculiaritats de l\'explorador d\'arxius pot ser que no funcioni correctament la primera execució. - Cancel·la Porta-retalls netejat. Temps d\'espera del porta-retalls Temps abans de netejar el porta-retalls després de copiar un usuari o contrasenya @@ -72,7 +71,6 @@ Massa passades. Establint a 2147483648. És necessari un títol. Insereix un enter positiu al camp longitud - Arxiu no trobat. Explorador d\'arxius Generar contrasenya confirma contrasenya diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3948869d2..f11a40238 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -34,7 +34,6 @@ Znovu neukázat Závorky Instalace správce souborů OpenIntents k procházení souborů - Storno Schránka vyčištěna Chyba schránky Některé Android telefony od Samsungu nedovolují aplikacím používat schránku. @@ -79,7 +78,6 @@ Do nastavení „Délka“ zadejte celé kladné číslo. Název pole Hodnota pole - Soubor nenalezen. Správce souborů Vytvoř heslo potvrď heslo diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 0b167661f..ac9d55474 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -34,7 +34,6 @@ Vis ikke igen Parenteser Installer OpenIntents Fil Manager for at gennemse filer - Annuller Udklipsholder ryddet Udklipsfejl Nogle Samsung Android-telefoner, vil ikke lade programmer bruge udklipsholderen. @@ -78,7 +77,6 @@ Angiv et positivt heltal i feltet \"Længde\". Feltnavn Feltværdi - Kunne ikke finde filen. Filhåndtering Generer adgangskode bekræft adgangskode diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3ab4f6a81..e459e4356 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,7 +36,6 @@ Nicht mehr anzeigen Klammern Durchsuchen Sie Ihre Dateien, indem Sie den OpenIntents File Manager installieren - Abbrechen Zwischenablage geleert Zwischenablagefehler Einige Samsung Android-Smartphones lassen keine Nutzung der Zwischenablage durch Apps zu. @@ -81,7 +80,6 @@ Eine positive ganze Zahl in das Feld „Länge“ eingeben. Feldname Feldwert - Datei nicht gefunden. Datei nicht gefunden. Bitte versuchen, sie über den Dateimanager zu öffnen. Dateimanager Passwort generieren diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index bafaee594..eaed388d2 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -32,7 +32,6 @@ Να μην ερωτηθώ ξανά Αγκύλες Η αναζήτηση αρχείων απαιτεί τον Διαχειριστή Αρχείων Open Intents, πατήστε παρακάτω για να τον εγκαταστήσετε. Λόγω μερικών ιδιορρυθμιών στον διαχειριστή αρχείων, η περιήγηση μπορεί να μην λειτουργεί σωστά την πρώτη φορά που θα περιηγηθείτε. - Ακύρωση Το πρόχειρο καθαρίστηκε. Σφάλμα προχείρου Μερικά Android κινητά τηλέφωνα της Samsung έχουν ένα σφάλμα στην εφαρμογή του προχείρου που προκαλεί την αντιγραφή από εφαρμογές να αποτυγχάνει. Για περισσότερες πληροφορίες πηγαίνετε: @@ -77,7 +76,6 @@ Εισάγετε έναν θετικό ακέραιο αριθμό στο πεδίο μήκους Όνομα Πεδίου Τιμή πεδίου - Το αρχείο δεν βρέθηκε. Διαχείριση Αρχείων Δημιουργία Κωδικού επιβεβαίωση κωδικού diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b3d025331..d3a94100c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -33,7 +33,6 @@ Configuración de la aplicación Paréntesis Explora ficheros con OpenIntents File Manager - Cancelar Portapapeles limpiado Portapapeles caducado Duración de almacemiento en el portapapeles @@ -71,7 +70,6 @@ Pasadas demasiado grande. Establecido a 2147483648. Se necesita un título. Introduzca un entero positivo en el campo longitud - Archivo no encontrado. Explorador de Archivos Generar Contraseña confirmar contraseña diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 27f553382..7a968d736 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -33,7 +33,6 @@ Ez erakutsi berriro Brackets Fitxategietan nabigatzeak Open Intents Fitxategi Kudeatzailea behar du. Klik egin azpian instalatzeko. Fitxategien kudeatzailaren arazo batzuk direla eta, izan daiteke nabigazioak ondo ez funtzionatzea lehenengo aldian. - Utzi Arbela ezabatuta. Arbelean errorea Samsung Android telefono batzuek akats bat daukate arbelaren inplementazioan, eta honen ondorioz aplikazioetatik kopiatzeak huts egiten du. Xehetasun gehiagotarako ondokora joan: @@ -78,7 +77,6 @@ Eremuaren luzeran entero positibo bat sartu Eremuaren izena Eremuaren balorea - Fitxategi ez aurkitua. Fitxategien nabigatzailea Pasahitza sortu pasahitza berretsi diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 3b2bb8be2..ec3196f1b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -32,7 +32,6 @@ Älä näytä enää uudelleen Hakasulkeet Tiedostojen selaus vaatii Open Intents File Manager -tiedostonhallintaohjelman, klikkaa alla olevaa linkkiä asentaaksesi sen. Joidenkin ominaisuuksien takia se ei ehkä toimi oikein ensimmäisellä käynnistyksellä. - Peruuta Leikepöytä tyhjennetty. Leikepöytävirhe Joissakin Android-puhelimissa on virhe leikepöydän toteutuksessa, mikä aiheuttaa kopioinnin epäonnistumisen. Lisätietoa: @@ -77,7 +76,6 @@ Syötä positiivinen kokonaisluku pituus-kenttään Kentän nimi Kentän arvo - Tiedostoa ei löydetty. Tiedostoselain Generoi salasana vahvista salasana diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 718d903f9..934953fe7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -36,7 +36,6 @@ Crochets ASCII étendu Parcourir les fichiers en installant le gestionnaire de fichiers OpenIntents - Annuler Autoriser Presse-papier vidé Erreur de presse-papier @@ -83,7 +82,6 @@ Impossible d’activer le service de remplissage automatique. Nom du champ Valeur du champ - Impossible de trouver le fichier. Impossible de trouver le fichier. Essayer de le rouvrir depuis votre gestionnaire de fichiers. Gestionnaire de fichiers Générer un mot de passe diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 03d105380..3d1a2f4f9 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -14,7 +14,6 @@ Non amosar de novo Parénteses ASCII extendido - Cancelar Permitir Portapapeis limpo Erro do portapapeis diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 048b63ef6..3ebaf1683 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -32,7 +32,6 @@ "Ne mutassa többet" Zárójelek Fájlok böngészése az OpenIntents fájlkezelő telepítésével - Mégse Vágólap törölve Vágólap hiba Egyes androidos Samsung telefonok nem engedik, hogy az alkalmazások használják a vágólapot. @@ -76,7 +75,6 @@ Írjon be egy pozitív egész számot a „Hossz” mezőbe. Mezőnév Mezőérték - A fájl nem található. A fájl nem található. Próbálja meg újra megnyitni a fájlkezelőben. Fájlkezelő Jelszó előállítása diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4d5a73bd6..3edc21733 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -33,7 +33,6 @@ Impostazioni app Parentesi Sfoglia i file installando il Gestore File di OpenIntents - Annulla Appunti eliminati Errore negli appunti Alcuni telefoni Android di Samsung non permettono alle app di usare gli appunti. @@ -77,7 +76,6 @@ Inserisci un numero naturale positivo nel campo \"lunghezza\". Nome campo Valore campo - File non trovato. File non trovato. Prova a riaprirlo dal tuo gestore di file. Gestore file Genera password diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 13446386d..cf95d67a9 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -31,7 +31,6 @@ אל תציג שוב סוגריים סייר הקבצים דורש את סייר הקבצים Open Intents, לחץ למטע כדי להתקין. בגלל מספר בעיות בסייר, ייתכן ויהיו בעיות בהפעלה הראשונה. - בטל לוח ההעתקה נוקה. שגיאת לוח ההעתקה במספר מכשירי אנרואיד מסמסונג קיים באג במימוש לוח ההעתקה שיכול לגרום לבעיות בהעתקה מהיישום. לעוד מידע עבור אל: @@ -74,7 +73,6 @@ הזן מספר חיובי בשדה האורך שם השדה ערך השדה - קובץ לא נמצא. סייר קבצים צור סיסמה אשר סיסמה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 616ecaa51..c4298a2c2 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -30,7 +30,6 @@ アプリケーション設定 カッコ ファイルを検索するには OI File Manager が必要です。 - キャンセル クリップボードを消去しました。 クリップボード タイムアウト コピーした情報をクリップボードから消去する時間 @@ -68,7 +67,6 @@ 値が大きすぎます。 2147483648にセットしました。 タイトルは必須入力です。 \"長さ\"欄には正の整数を入力してください。 - ファイルが見つかりません。 ファイルブラウザ パスワードを生成する パスワードをもう一度入力 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 7f476de45..5d30cb9e8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -35,7 +35,6 @@ 브라켓 확장 ASCII OpenIntents File Manager를 설치하여 파일 찾아보기 - 취소 허가 클립보드 비워짐 클립보드 오류 @@ -85,7 +84,6 @@ 그룹을 자신에게 옮길 수 없습니다. 필드 이름 필드 값 - 파일을 찾을 수 없습니다. 파일을 찾을 수 없습니다. 파일 탐색기에서 열리는지 확인해 주세요. 파일 탐색기 비밀번호 생성 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 6a733d516..ff9a7e1f2 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -2,7 +2,6 @@ KeePass DX yra KeePass slaptažodžių tvarkyklės realizacija Android platformai Iškarpinė išvalyta. - Failas nerastas. Neteisingas slaptažodis arba rakto failas. Atsiliepimai: Pagrindinis puslapis: @@ -15,7 +14,6 @@ Programėlės nustatymai Daugiau neberodyti Skliaustai - Atšaukti Duomenų bazė Skaitmenys Atšaukti diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 579e34c32..b3b6be234 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -14,7 +14,6 @@ Turpmāk nerādīt Iekavas Failu pārlūkošanai nepieciešams pārlūks. - Atcelt Starpliktuve notīrīta Starpliktuves kļūda Dažiem Samsung tālruņiem ir problēmas ar starpliktuves lietošanu. Lai saņemtu sīkāku informāciju, dodieties uz: @@ -57,7 +56,6 @@ Norādiet garumu lielāku par nulli Lauka nosaukums Lauka vērtība - Fails nav atrasts. Failu pārlūks Ģenerēt Paroli apstipriniet paroli diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 29c250cd1..f913060f1 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -35,7 +35,6 @@ Parenteser Utvidet ASCII Utforsk filer ved å installere OpenIntents-filbehandleren - Avbryt Tillat Utklippstavle tømt Utklippstavlefeil @@ -85,7 +84,6 @@ Kan ikke flytte gruppe inn i seg selv. Feltnavn Feltverdi - Fant ikke filen. Fant ikke filen. Prøv å åpne den fra din innholdsleverandør. Filutforsker Opprett passord diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f44cc2ff6..f1607444c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -33,7 +33,6 @@ App-instellingen Haakjes Zoek een bestand op door het installeren van de OpenIntents File Manager - Annuleren Klembord gewist Klembordtime-out Tijd van opslag op het klembord @@ -71,7 +70,6 @@ \"Cycli-waarde\" te groot. Wordt ingesteld op 2147483648. Voeg een titel toe. Voer een positief geheel getal in in het veld \"Lengte\". - Bestand niet gevonden. Bestandsverkenner Wachtwoord genereren wachtwoord bevestigen diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 0d480cd4f..3ea0729eb 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -31,7 +31,6 @@ Programinnstillingar Parentesar Du må ha Open Intents filbehandlar for å kunna bla i filer. Klikk nedanfor for å installera han. Grunna nokre småfeil i programmet kan det vera at det ikkje fungerer heilt den første gongen du bruker det. - Avbryt Utklippstavla er tømt. Tidsavbrot på utklippstavla Tid før utklippstavla blir tømt etter at brukarnamnet eller passordet er kopiert. @@ -69,7 +68,6 @@ For mange omgangar. Bruker 2147483648. Treng ein tittel. Bruk eit positivt heiltal i lengdfeltet - Fann ikkje fila. Filbehandlar Lag passord stadfest passordet diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7af14e69e..78c8b9304 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -31,7 +31,6 @@ Ustawienia aplikacji Nawiasy Przeglądaj pliki, instalując Menedżera plików OpenIntents - Anuluj Schowek został wyczyszczony Czas wygaśnięcia schowka Czas przechowywania w schowku @@ -68,7 +67,6 @@ \"Rundy szyfrowania\" są zbyt wysokie. Ustaw na 2147483648. Dodaj tytuł. Wprowadź dodatnią liczbę całkowitą w polu \"Długość\". - Nie znaleziono pliku. Przeglądarka plików Generuj hasło potwierdź hasło diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e0e309ddc..1caf84b35 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -33,7 +33,6 @@ Configurações do aplicativo Parênteses Procure arquivos instalando o OpenIntents File Manager - Cancelar Área de transferência limpa Tempo limite para o clipboard Duração do armazenamento na área de transferência @@ -71,7 +70,6 @@ \"Número de rodadas\" é muito grande. Modificado para 2147483648. Insira um título. Digite um número inteiro positivo no campo \"Tamanho\". - Não pôde encontrar o arquivo. Localizador de arquivos Gerar senha confirmar senha diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 4337182db..bbfba6a7c 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -32,7 +32,6 @@ Não mostrar novamente Parênteses Explore ficheiros instalando o gestor de ficheiros OpenIntents - Cancelar Área de transferência limpa Erro na área de transferência Alguns dispositivos Samsung Android não deixam as apps usarem a área de transferência. @@ -77,7 +76,6 @@ Digite um número inteiro positivo no campo \"Tamanho\". Nome do campo Valor do campo - Não pôde encontrar o ficheiro. Arquivo não encontrado. Tente reabrí-lo de seu provedor de conteúdo. Localizador de ficheiros Gerar palavra-chave diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9d437326a..b7ce3614b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -32,7 +32,6 @@ Не показывать снова {[(Скобки)]} Для обзора файлов установите OpenIntents File Manager - Отмена Буфер обмена очищен Ошибка буфера обмена Некоторые устройства Samsung не дают приложению использовать буфер обмена. @@ -77,7 +76,6 @@ Поле \"Длина\" должно быть положительным целым числом. Название поля Значение поля - Файл не найден. Файл не найден. Попробуйте повторно открыть через встроенный поставщик содержимого. Обзор файлов Генерация пароля diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 7fcf15cc4..1d3ec2b72 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -30,7 +30,6 @@ Nastavenia aplikácie Konzoly Prezeranie súborov vyžaduje otvorenie Správcu súborov, kliknite nižšie pre inštalovanie. Kôli chybám v správcovi súborov, prehľadávanie nemusí pracovať správne, ak prehľadávate prvý krát. - Zrušiť Schránka vyčistená. Timeout Schránky Čas uchovania v schránke @@ -68,7 +67,6 @@ Príliš veľa opakovaní. Nastavujem na 2147483648. Vyžaduje sa názov. Zadajte celé kladné číslo na dĺžku poľa - Súbor nenájdený. Správca Súborov Generovať Heslo potvrdiť heslo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index dae3dab6e..269f46802 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -33,7 +33,6 @@ Visa inte igen Parenteser Filhantering kräver Open Intents File Manager, klicka nedan för att installera. Filhanteraren kanske inte fungerar korrekt vid första användningen. - Avbryt Urklippet är rensat. Urklippsfel Vissa Samsung-telefoner har en bugg som gör att applikationer inte kan kopiera till urklipp. För mer detaljer, gå till: @@ -77,7 +76,6 @@ Ange ett positivt heltal i fältet för längd Fältnamn Fältvärde - Filen hittades inte. Filhanterare Generera lösenord bekräfta lösenord diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 832942798..e3284f354 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -35,7 +35,6 @@ Parantez Genişletilmiş ASCII OpenIntents Dosya Yöneticisi\'ni yükleyerek dosyalara göz atın - İptal İzin ver Pano temizlendi Pano hatası @@ -85,7 +84,6 @@ Bir grubu kendine taşıyamazsın. Alan adı Alan değeri - Dosya bulunamadı. Dosya bulunamadı. Dosya tarayıcınızda yeniden açmayı deneyin. Dosya tarayıcı Parola üret diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8cb761c40..e6a70ad6b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -31,7 +31,6 @@ Налаштування програми Дужки Для перегляду файла необхідно Open Intents File Manager, натисніть нижче для його інсталяції. У зв’язку з деякими недоробками у менеджері файлів перегляд може працювати некоректно при запуску перший раз. - Відміна Буфер обміну очищено. Тайм-аут буфера обміну Час через який буде очищено буфер обміну після копіювання ім’я користувача чи пароля @@ -69,7 +68,6 @@ Надто багато циклів. Установлено 2147483648. Необхідно вказати заголовок. Введіть ціле число на усю довжину поля - Файл не знайдено. Перегляд файлів Згенерувати пароль підтвердження пароля diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 11c50efec..fcd56c444 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -31,7 +31,6 @@ 应用设置 括号 安装 OpenIntents File Manager 应用来选取文件 - 取消 剪贴板已清空 剪贴板清空延时 剪贴板保存时间 @@ -69,7 +68,6 @@ “变换次数”过多。已设置为 2147483648。 请添加标题。 请在“长度”字段输入一个正整数。 - 找不到文件。 文件浏览器 生成密码 确认密码 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7c17ea228..5cfc8d24d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -30,7 +30,6 @@ 應用程式設定 括弧 流覽檔需要安裝Open Intents File Manager軟體,點下面安裝。由於一些檔管理軟體的差異,在你第一次瀏覽時可能無法正常工作。 - 取消 剪貼板已清除 剪貼板超時 複製用戶名或密碼到剪貼板後清除的時間 @@ -68,7 +67,6 @@ 次數太多。最大設置到2147483648。 標題為必填。 長度欄位輸入一個正整數 - 文件未找到。 檔案管理器 生成密碼 確認密碼 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 28e6fea44..1bcc0cd0e 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -148,16 +148,20 @@ settings_database_security_key settings_database_credentials_key - database_general_key + database_category_general_key database_name_key database_description_key database_default_username_key database_custom_color_key database_version_key - database_history_key + database_category_compression_key database_data_compression_key + + database_category_recycle_bin_key recycle_bin_key + + database_category_history_key max_history_items_key max_history_size_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 767e34007..bdcead124 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,6 @@ Brackets Extended ASCII Create, Open and Save a database file requires installing a file manager that accepts the Intent action ACTION_CREATE_DOCUMENT and ACTION_OPEN_DOCUMENT - Cancel Allow Clipboard cleared Clipboard error @@ -101,6 +100,7 @@ Could not load your database. Could not load the key. Try to lower the KDF \"Memory Usage\". At least one password generation type must be selected. + At lest one credential must be set. The passwords do not match. \"Transformation rounds\" too high. Setting to 2147483648. Each string must have a field name. @@ -113,7 +113,6 @@ Unable to create database with this password and key file. Field name Field value - Could not find file. Could not find file. Try reopening it from your file browser. File browser Generate password @@ -128,6 +127,7 @@ Install from Play Store Could not read password or keyfile. Wrong algorithm. + %1$s with the same UUID %2$s already exists. Could not recognize the database format. No keyfile exists. The keyfile is empty. @@ -183,6 +183,8 @@ Write-protected KeePass DX needs write permission in order to change anything in your database. Starting with Android KitKat, some devices no longer allow apps to write to the SD card. + The database contains duplicate UUIDs. + By validating this dialog, KeePass DX will fix the problem (by generating new UUIDs for duplicates) and continue. Selection mode Recent file history Remember recent filenames diff --git a/app/src/main/res/xml/preferences_database.xml b/app/src/main/res/xml/preferences_database.xml index 4eadcce9b..42772b401 100644 --- a/app/src/main/res/xml/preferences_database.xml +++ b/app/src/main/res/xml/preferences_database.xml @@ -21,7 +21,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"> - - - + android:title="@string/database_custom_color_title" + android:summary="[color]" + chroma:chromaShapePreview="ROUNDED_SQUARE" + chroma:chromaColorMode="RGB" + chroma:chromaIndicatorMode="HEX" />