diff --git a/CHANGELOG b/CHANGELOG index 0fdaf8a2d..973ad31d2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +KeePassDX(3.5.0) + * Support YubiKey challenge-response #8 #137 + * Better exception management during database save #1346 + * Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX) + * Hide clipboard sensitive text when copy entry field #1386 + * Fix attachment download button #1401 + * Add monochrome icon #1403 #1404 (Thx @Sandelinos) + * Fix lock with back button #1412 #1414 (Thx @ryg-git) + * Vanadium compatibility #1447 (Thx @flawedworld) + KeePassDX(3.4.5) * Fix custom data in group (fix KeeShare) #1335 * Fix device credential unlocking #1344 diff --git a/Gemfile.lock b/Gemfile.lock index f576a4052..bc807e7a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,25 +3,25 @@ GEM specs: CFPropertyList (3.0.5) rexml - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.577.0) - aws-sdk-core (3.130.1) + aws-partitions (1.646.0) + aws-sdk-core (3.160.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.55.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.58.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.0) + aws-sdk-s3 (1.114.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -34,10 +34,10 @@ GEM rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) + dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.92.2) - faraday (1.10.0) + excon (0.93.0) + faraday (1.10.2) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -56,8 +56,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -66,7 +66,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.205.1) + fastlane (2.210.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -107,9 +107,9 @@ GEM xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-versioning_android (0.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.19.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.2) + google-apis-androidpublisher_v3 (0.29.0) + google-apis-core (>= 0.9.0, < 2.a) + google-apis-core (0.9.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -118,27 +118,27 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.10.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.7.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.13.0) - google-apis-core (>= 0.4, < 2.a) + google-apis-iamcredentials_v1 (0.15.0) + google-apis-core (>= 0.9.0, < 2.a) + google-apis-playcustomapp_v1 (0.11.0) + google-apis-core (>= 0.9.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.2.0) - google-cloud-storage (1.36.1) + google-cloud-errors (1.3.0) + google-cloud-storage (1.43.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.2) + googleauth (1.2.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -146,12 +146,12 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.1) - json (2.6.1) - jwt (2.3.0) + json (2.6.2) + jwt (2.5.0) memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) @@ -162,9 +162,9 @@ GEM optparse (0.1.1) os (1.1.4) plist (3.6.0) - public_suffix (4.0.7) + public_suffix (5.0.0) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -174,9 +174,9 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.1) + signet (0.17.0) addressable (~> 2.8) - faraday (>= 0.17.5, < 3.0) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -193,11 +193,11 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8.1) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcodeproj (1.21.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/README.md b/README.md index 8b9d32ce1..b1d70da7b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - Create database files / entries and groups. - Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm. - - **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …). + - **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …). - Allows opening and **copying URI / URL fields quickly**. - **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*. - **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA). @@ -74,7 +74,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w ## License - Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). + Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). This file is part of KeePassDX. diff --git a/app/build.gradle b/app/build.gradle index cd5f7ea0b..c91996b74 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 32 - versionCode = 114 - versionName = "3.4.5" + versionCode = 118 + versionName = "3.5.0" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" @@ -93,7 +93,7 @@ android { } } -def room_version = "2.4.2" +def room_version = "2.4.3" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" @@ -101,14 +101,14 @@ dependencies { implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.media:media:1.6.0' // Lifecycle - LiveData - ViewModel - Coroutines implementation "androidx.core:core-ktx:$android_core_version" - implementation 'androidx.fragment:fragment-ktx:1.4.1' + implementation 'androidx.fragment:fragment-ktx:1.5.2' implementation "com.google.android.material:material:$android_material_version" // Token auto complete // From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed diff --git a/app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/2.json b/app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/2.json new file mode 100644 index 000000000..a6e3f6fac --- /dev/null +++ b/app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/2.json @@ -0,0 +1,90 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "f8fb4aed546de19ae7ca0797f49b26a4", + "entities": [ + { + "tableName": "file_database_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))", + "fields": [ + { + "fieldPath": "databaseUri", + "columnName": "database_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "databaseAlias", + "columnName": "database_alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyFileUri", + "columnName": "keyfile_uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hardwareKey", + "columnName": "hardware_key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "database_uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "cipher_database", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))", + "fields": [ + { + "fieldPath": "databaseUri", + "columnName": "database_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedValue", + "columnName": "encrypted_value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "specParameters", + "columnName": "specs_parameters", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "database_uri" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')" + ] + } +} \ No newline at end of file diff --git a/app/src/free/res/drawable-v24/ic_launcher_monochrome.xml b/app/src/free/res/drawable-v24/ic_launcher_monochrome.xml new file mode 100644 index 000000000..9c82835ee --- /dev/null +++ b/app/src/free/res/drawable-v24/ic_launcher_monochrome.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml b/app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml new file mode 100644 index 000000000..c0491a088 --- /dev/null +++ b/app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index df3349851..f7ab6482b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -156,6 +156,9 @@ android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" /> + (R.id.activity_about_privacy_text).apply { + movementMethod = LinkMovementMethod.getInstance() + text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy), + HtmlCompat.FROM_HTML_MODE_LEGACY) + } + findViewById(R.id.activity_about_contribution_text).apply { movementMethod = LinkMovementMethod.getInstance() text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution), 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 9ad584793..e10721e04 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -55,7 +55,8 @@ import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService @@ -66,6 +67,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.asError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import java.io.FileNotFoundException @@ -155,8 +157,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> launchPasswordActivity( - databaseFileUri, - fileDatabaseHistoryEntityToOpen.keyFileUri + databaseFileUri, + fileDatabaseHistoryEntityToOpen.keyFileUri, + fileDatabaseHistoryEntityToOpen.hardwareKey ) } } @@ -250,7 +253,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), ?: MainCredential() databaseFilesViewModel.addDatabaseFile( databaseUri, - mainCredential.keyFileUri + mainCredential.keyFileUri, + mainCredential.hardwareKey ) } } @@ -268,18 +272,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), launchGroupActivityIfLoaded(database) } } - } else { - var resultError = "" - val resultMessage = result.message - // Show error message - if (resultMessage != null && resultMessage.isNotEmpty()) { - resultError = "$resultError $resultMessage" - } - Log.e(TAG, resultError) - Snackbar.make(coordinatorLayout, - resultError, - Snackbar.LENGTH_LONG).asError().show() } + coordinatorLayout.showActionErrorIfNeeded(result) } /** @@ -297,10 +291,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() } - private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { + private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) { MainCredentialActivity.launch(this, databaseUri, keyFile, + hardwareKey, { exception -> fileNoFoundAction(exception) }, @@ -321,7 +316,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), } private fun launchPasswordActivityWithPath(databaseUri: Uri) { - launchPasswordActivity(databaseUri, null) + launchPasswordActivity(databaseUri, null, null) // Delete flickering for kitkat <= if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) overridePendingTransition(0, 0) 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 ee746d966..b658afdcd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -69,7 +69,7 @@ import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.model.GroupInfo -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK @@ -1368,7 +1368,6 @@ class GroupActivity : DatabaseLockActivity(), EntrySelectionHelper.removeInfoFromIntent(intent) if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { lockAndExit() - super.onRegularBackPressed() } else { backToTheAppCaller() } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index fad63269c..371a321ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -56,9 +56,11 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY @@ -73,6 +75,7 @@ import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.asError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import java.io.FileNotFoundException @@ -101,6 +104,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private var mRememberKeyFile: Boolean = false private var mExternalFileHelper: ExternalFileHelper? = null + private var mRememberHardwareKey: Boolean = false + private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false @@ -133,11 +138,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu PreferencesUtil.enableReadOnlyDatabase(this) } mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) + mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this) - mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity) + // Build elements to manage keyfile selection + mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper?.buildOpenDocument { uri -> if (uri != null) { - mainCredentialView?.populateKeyFileTextView(uri) + mainCredentialView?.populateKeyFileView(uri) } } mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) @@ -171,6 +178,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu mAdvancedUnlockViewModel.checkUnlockAvailability() enableConfirmationButton() } + mainCredentialView?.onKeyFileChecked = + CompoundButton.OnCheckedChangeListener { _, _ -> + // TODO mAdvancedUnlockViewModel.checkUnlockAvailability() + enableConfirmationButton() + } + mainCredentialView?.onHardwareKeyChecked = + CompoundButton.OnCheckedChangeListener { _, _ -> + // TODO mAdvancedUnlockViewModel.checkUnlockAvailability() + enableConfirmationButton() + } // Observe if default database mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> @@ -204,10 +221,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu databaseKeyFileUri } + val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey + val hardwareKey = + if (mRememberHardwareKey + && databaseHardwareKey == null) { + databaseFile?.hardwareKey + } else { + databaseHardwareKey + } + // Define title filenameView?.text = databaseFile?.databaseAlias ?: "" - onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) + onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey) } } @@ -215,6 +241,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu super.onResume() mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity) + mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity) // Back to previous keyboard is setting activated if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) { @@ -266,90 +293,84 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu launchGroupActivityIfLoaded(database) } else { mainCredentialView?.requestPasswordFocus() + // Manage special exceptions + when (result.exception) { + is DuplicateUuidDatabaseException -> { + // Relaunch loading if we need to fix UUID + showLoadDatabaseDuplicateUuidMessage { + + var databaseUri: Uri? = null + var mainCredential = MainCredential() + var readOnly = true + var cipherEncryptDatabase: CipherEncryptDatabase? = null + + result.data?.let { resultData -> + databaseUri = resultData.getParcelable(DATABASE_URI_KEY) + mainCredential = + resultData.getParcelable(MAIN_CREDENTIAL_KEY) + ?: mainCredential + readOnly = resultData.getBoolean(READ_ONLY_KEY) + cipherEncryptDatabase = + resultData.getParcelable(CIPHER_DATABASE_KEY) + } - var resultError = "" - val resultException = result.exception - val resultMessage = result.message - - if (resultException != null) { - resultError = resultException.getLocalizedMessage(resources) - - when (resultException) { - is DuplicateUuidDatabaseException -> { - // Relaunch loading if we need to fix UUID - showLoadDatabaseDuplicateUuidMessage { - - var databaseUri: Uri? = null - var mainCredential = MainCredential() - var readOnly = true - var cipherEncryptDatabase: CipherEncryptDatabase? = null - - result.data?.let { resultData -> - databaseUri = resultData.getParcelable(DATABASE_URI_KEY) - mainCredential = - resultData.getParcelable(MAIN_CREDENTIAL_KEY) - ?: mainCredential - readOnly = resultData.getBoolean(READ_ONLY_KEY) - cipherEncryptDatabase = - resultData.getParcelable(CIPHER_DATABASE_KEY) - } - - databaseUri?.let { databaseFileUri -> - showProgressDialogAndLoadDatabase( - databaseFileUri, - mainCredential, - readOnly, - cipherEncryptDatabase, - true - ) - } + databaseUri?.let { databaseFileUri -> + showProgressDialogAndLoadDatabase( + databaseFileUri, + mainCredential, + readOnly, + cipherEncryptDatabase, + true + ) } } - is FileNotFoundDatabaseException -> { - // Remove this default database inaccessible - if (mDefaultDatabase) { - mDatabaseFileViewModel.removeDefaultDatabase() - } + } + is FileNotFoundDatabaseException -> { + // Remove this default database inaccessible + if (mDefaultDatabase) { + mDatabaseFileViewModel.removeDefaultDatabase() } } } - - // Show error message - if (resultMessage != null && resultMessage.isNotEmpty()) { - resultError = "$resultError $resultMessage" - } - Log.e(TAG, resultError) - Snackbar.make( - coordinatorLayout, - resultError, - Snackbar.LENGTH_LONG - ).asError().show() } } } + coordinatorLayout.showActionErrorIfNeeded(result) } private fun getUriFromIntent(intent: Intent?) { // If is a view intent val action = intent?.action - if (action != null - && action == VIEW_INTENT) { - mDatabaseFileUri = intent.data - mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) + if (action == VIEW_INTENT) { + fillCredentials( + intent.data, + UriUtil.getUriFromIntent(intent, KEY_KEYFILE), + HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY)) + ) } else { - mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) - intent?.getParcelableExtra(KEY_KEYFILE)?.let { - mainCredentialView?.populateKeyFileTextView(it) - } + fillCredentials( + intent?.getParcelableExtra(KEY_FILENAME), + intent?.getParcelableExtra(KEY_KEYFILE), + HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY)) + ) } try { intent?.removeExtra(KEY_KEYFILE) + intent?.removeExtra(KEY_HARDWARE_KEY) } catch (e: Exception) {} mDatabaseFileUri?.let { mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) } } + private fun fillCredentials(databaseUri: Uri?, + keyFileUri: Uri?, + hardwareKey: HardwareKey?) { + mDatabaseFileUri = databaseUri + mainCredentialView?.populateKeyFileView(keyFileUri) + mainCredentialView?.populateHardwareKeyView(hardwareKey) + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) getUriFromIntent(intent) @@ -358,7 +379,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private fun launchGroupActivityIfLoaded(database: Database) { // Check if database really loaded if (database.loaded) { - clearCredentialsViews(true) + clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true) GroupActivity.launch(this, database, { onValidateSpecialMode() }, @@ -408,7 +429,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential() when (cipherDecryptDatabase.credentialStorage) { CredentialStorage.PASSWORD -> { - mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue) + mainCredential.password = String(cipherDecryptDatabase.decryptedValue) } CredentialStorage.KEY_FILE -> { // TODO advanced unlock key file @@ -423,14 +444,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu ) } - private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { + private fun onDatabaseFileLoaded(databaseFileUri: Uri?, + keyFileUri: Uri?, + hardwareKey: HardwareKey?) { // Define Key File text if (mRememberKeyFile) { - mainCredentialView?.populateKeyFileTextView(keyFileUri) + mainCredentialView?.populateKeyFileView(keyFileUri) + } + + // Define hardware key + if (mRememberHardwareKey) { + mainCredentialView?.populateHardwareKeyView(hardwareKey) } // Define listener for validate button - confirmButtonView?.setOnClickListener { loadDatabase() } + confirmButtonView?.setOnClickListener { + mainCredentialView?.validateCredential() + } // If Activity is launch with a password and want to open directly val intent = intent @@ -462,10 +492,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu } } - private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { + private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile, + clearHardwareKey: Boolean = !mRememberHardwareKey) { mainCredentialView?.populatePasswordTextView(null) if (clearKeyFile) { - mainCredentialView?.populateKeyFileTextView(null) + mainCredentialView?.populateKeyFileView(null) + } + if (clearHardwareKey) { + mainCredentialView?.populateHardwareKeyView(null) } } @@ -656,18 +690,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private const val KEY_FILENAME = "fileName" private const val KEY_KEYFILE = "keyFile" + private const val KEY_HARDWARE_KEY = "hardwareKey" private const val VIEW_INTENT = "android.intent.action.VIEW" private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_PASSWORD = "password" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" - private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, + private fun buildAndLaunchIntent(activity: Activity, + databaseFile: Uri, + keyFile: Uri?, + hardwareKey: HardwareKey?, intentBuildLauncher: (Intent) -> Unit) { val intent = Intent(activity, MainCredentialActivity::class.java) intent.putExtra(KEY_FILENAME, databaseFile) if (keyFile != null) intent.putExtra(KEY_KEYFILE, keyFile) + if (hardwareKey != null) + intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString()) intentBuildLauncher.invoke(intent) } @@ -680,8 +720,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu @Throws(FileNotFoundException::class) fun launch(activity: Activity, databaseFile: Uri, - keyFile: Uri?) { - buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + keyFile: Uri?, + hardwareKey: HardwareKey?) { + buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> activity.startActivity(intent) } } @@ -696,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launchForSearchResult(activity: Activity, databaseFile: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, searchInfo: SearchInfo) { - buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> EntrySelectionHelper.startActivityForSearchModeResult( activity, intent, @@ -715,8 +757,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launchForSaveResult(activity: Activity, databaseFile: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, searchInfo: SearchInfo) { - buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> EntrySelectionHelper.startActivityForSaveModeResult( activity, intent, @@ -734,8 +777,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launchForKeyboardResult(activity: Activity, databaseFile: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, searchInfo: SearchInfo?) { - buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( activity, intent, @@ -754,10 +798,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launchForAutofillResult(activity: AppCompatActivity, databaseFile: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, activityResultLauncher: ActivityResultLauncher?, autofillComponent: AutofillComponent, searchInfo: SearchInfo?) { - buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> AutofillHelper.startActivityForAutofillResult( activity, intent, @@ -775,8 +820,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launchForRegistration(activity: Activity, databaseFile: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, registerInfo: RegisterInfo?) { - buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> + buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent -> EntrySelectionHelper.startActivityForRegistrationModeResult( activity, intent, @@ -792,6 +838,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launch(activity: AppCompatActivity, databaseUri: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, fileNoFoundAction: (exception: FileNotFoundException) -> Unit, onCancelSpecialMode: () -> Unit, onLaunchActivitySpecialMode: () -> Unit, @@ -800,43 +847,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu try { EntrySelectionHelper.doSpecialAction(activity.intent, { - MainCredentialActivity.launch(activity, - databaseUri, keyFile) + launch( + activity, + databaseUri, + keyFile, + hardwareKey + ) }, { searchInfo -> // Search Action - MainCredentialActivity.launchForSearchResult(activity, - databaseUri, keyFile, - searchInfo) + launchForSearchResult( + activity, + databaseUri, + keyFile, + hardwareKey, + searchInfo + ) onLaunchActivitySpecialMode() }, { searchInfo -> // Save Action - MainCredentialActivity.launchForSaveResult(activity, - databaseUri, keyFile, - searchInfo) + launchForSaveResult( + activity, + databaseUri, + keyFile, + hardwareKey, + searchInfo + ) onLaunchActivitySpecialMode() }, { searchInfo -> // Keyboard Selection Action - MainCredentialActivity.launchForKeyboardResult(activity, - databaseUri, keyFile, - searchInfo) + launchForKeyboardResult( + activity, + databaseUri, + keyFile, + hardwareKey, + searchInfo + ) onLaunchActivitySpecialMode() }, { searchInfo, autofillComponent -> // Autofill Selection Action if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - MainCredentialActivity.launchForAutofillResult(activity, - databaseUri, keyFile, - autofillActivityResultLauncher, - autofillComponent, - searchInfo) + launchForAutofillResult( + activity, + databaseUri, + keyFile, + hardwareKey, + autofillActivityResultLauncher, + autofillComponent, + searchInfo + ) onLaunchActivitySpecialMode() } else { onCancelSpecialMode() } }, { registerInfo -> // Registration Action - MainCredentialActivity.launchForRegistration(activity, - databaseUri, keyFile, - registerInfo) + launchForRegistration( + activity, + databaseUri, + keyFile, + hardwareKey, + registerInfo + ) onLaunchActivitySpecialMode() } ) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt index d45c3ea97..dea4f73c9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt @@ -27,7 +27,7 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.MainCredentialView @@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() { mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper?.buildOpenDocument { uri -> if (uri != null) { - mainCredentialView?.populateKeyFileTextView(uri) + mainCredentialView?.populateKeyFileView(uri) } } mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) 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 df32460cb..745543d3d 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 @@ -26,7 +26,7 @@ import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential class PasswordEncodingDialogFragment : DialogFragment() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ProFeatureDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ProFeatureDialogFragment.kt index dcd70366c..ac0293b63 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ProFeatureDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ProFeatureDialogFragment.kt @@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() { stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n") stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY)) builder.setPositiveButton(R.string.download) { _, _ -> - UriUtil.gotoUrl(requireContext(), R.string.app_pro_url) + UriUtil.gotoUrl(activity, + activity.getString(R.string.play_store_url, + activity.getString(R.string.keepro_app_id)) + ) } } else { stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n") stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY)) builder.setPositiveButton(R.string.contribute) { _, _ -> - UriUtil.gotoUrl(requireContext(), R.string.contribution_url) + UriUtil.gotoUrl(activity, R.string.contribution_url) } } builder.setMessage(stringBuilder) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt index e654c0817..4ba21490e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt @@ -35,9 +35,12 @@ import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.hardware.HardwareKeyActivity +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.view.HardwareKeySelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.PassKeyView import com.kunzisoft.keepass.view.applyFontVisibility @@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private var mMasterPassword: String? = null - private var mKeyFile: Uri? = null + private var mKeyFileUri: Uri? = null + private var mHardwareKey: HardwareKey? = null - private var rootView: View? = null + private lateinit var rootView: View - private var passwordCheckBox: CompoundButton? = null + private lateinit var passwordCheckBox: CompoundButton + private lateinit var passwordView: PassKeyView + private lateinit var passwordRepeatTextInputLayout: TextInputLayout + private lateinit var passwordRepeatView: TextView - private var passKeyView: PassKeyView? = null - private var passwordRepeatTextInputLayout: TextInputLayout? = null - private var passwordRepeatView: TextView? = null + private lateinit var keyFileCheckBox: CompoundButton + private lateinit var keyFileSelectionView: KeyFileSelectionView - private var keyFileCheckBox: CompoundButton? = null - private var keyFileSelectionView: KeyFileSelectionView? = null + private lateinit var hardwareKeyCheckBox: CompoundButton + private lateinit var hardwareKeySelectionView: HardwareKeySelectionView private var mListener: AssignMainCredentialDialogListener? = null @@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private var mNoKeyConfirmationDialog: AlertDialog? = null private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null + private var mAllowNoMasterKey: Boolean = false + private val passwordTextWatcher = object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun afterTextChanged(editable: Editable) { - passwordCheckBox?.isChecked = true + passwordCheckBox.isChecked = true } } @@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { 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) + mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false) } val builder = AlertDialog.Builder(activity) @@ -128,63 +135,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { .setPositiveButton(android.R.string.ok) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> } - rootView?.findViewById(R.id.credentials_information)?.setOnClickListener { + rootView.findViewById(R.id.credentials_information)?.setOnClickListener { UriUtil.gotoUrl(activity, R.string.credentials_explanation_url) } - passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) - passKeyView = rootView?.findViewById(R.id.password_view) - passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) - passwordRepeatView = rootView?.findViewById(R.id.password_confirmation) - passwordRepeatView?.applyFontVisibility() + passwordCheckBox = rootView.findViewById(R.id.password_checkbox) + passwordView = rootView.findViewById(R.id.password_view) + passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout) + passwordRepeatView = rootView.findViewById(R.id.password_confirmation) + passwordRepeatView.applyFontVisibility() + + keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox) + keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection) - keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) - keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) + hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox) + hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection) mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper?.buildOpenDocument { uri -> uri?.let { pathUri -> UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile -> - keyFileSelectionView?.error = null - keyFileCheckBox?.isChecked = true - keyFileSelectionView?.uri = pathUri + keyFileSelectionView.error = null + keyFileCheckBox.isChecked = true + keyFileSelectionView.uri = pathUri if (lengthFile <= 0L) { showEmptyKeyFileConfirmationDialog() } } } } - keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) + keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper) + + hardwareKeySelectionView.selectionListener = { hardwareKey -> + hardwareKeyCheckBox.isChecked = true + hardwareKeySelectionView.error = + if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) { + // show hardware driver dialog if required + getString(R.string.error_driver_required, hardwareKey.toString()) + } else { + null + } + } val dialog = builder.create() + dialog.setOnShowListener { dialog1 -> + val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) + positiveButton.setOnClickListener { - if (passwordCheckBox != null && keyFileCheckBox!= null) { - dialog.setOnShowListener { dialog1 -> - val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) - positiveButton.setOnClickListener { - - mMasterPassword = "" - mKeyFile = null - - var error = verifyPassword() || verifyKeyFile() - if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { - error = true - if (allowNoMasterKey) - showNoKeyConfirmationDialog() - else { - passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials) - } - } - if (!error) { - mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) - dismiss() - } - } - val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) - negativeButton.setOnClickListener { - mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential()) - dismiss() - } + mMasterPassword = "" + mKeyFileUri = null + mHardwareKey = null + + approveMainCredential() + } + val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) + negativeButton.setOnClickListener { + mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential()) + dismiss() } } @@ -194,78 +201,122 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { return super.onCreateDialog(savedInstanceState) } - private fun retrieveMainCredential(): MainCredential { - val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null - val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null - return MainCredential(masterPassword, keyFile) - } - - override fun onResume() { - super.onResume() - - // To check checkboxes if a text is present - passKeyView?.addTextChangedListener(passwordTextWatcher) - } - - override fun onPause() { - super.onPause() - - passKeyView?.removeTextChangedListener(passwordTextWatcher) + private fun approveMainCredential() { + val errorPassword = verifyPassword() + val errorKeyFile = verifyKeyFile() + val errorHardwareKey = verifyHardwareKey() + // Check all to fill error + var error = errorPassword || errorKeyFile || errorHardwareKey + val hardwareKey = hardwareKeySelectionView.hardwareKey + if (!error + && (!passwordCheckBox.isChecked) + && (!keyFileCheckBox.isChecked) + && (!hardwareKeyCheckBox.isChecked) + ) { + error = true + if (mAllowNoMasterKey) { + // show no key dialog if required + showNoKeyConfirmationDialog() + } else { + passwordRepeatTextInputLayout.error = + getString(R.string.error_disallow_no_credentials) + } + } else if (!error + && mMasterPassword.isNullOrEmpty() + && !keyFileCheckBox.isChecked + && !hardwareKeyCheckBox.isChecked + ) { + // show empty password dialog if required + error = true + showEmptyPasswordConfirmationDialog() + } else if (!error + && hardwareKey != null + && !HardwareKeyActivity.isHardwareKeyAvailable( + requireActivity(), hardwareKey, false) + ) { + // show hardware driver dialog if required + error = true + hardwareKeySelectionView.error = + getString(R.string.error_driver_required, hardwareKey.toString()) + } + if (!error) { + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) + dismiss() + } } private fun verifyPassword(): Boolean { var error = false - if (passwordCheckBox != null - && passwordCheckBox!!.isChecked - && passKeyView != null - && passwordRepeatView != null) { - mMasterPassword = passKeyView!!.passwordString - val confPassword = passwordRepeatView!!.text.toString() + passwordRepeatTextInputLayout.error = null + if (passwordCheckBox.isChecked) { + mMasterPassword = passwordView.passwordString + val confPassword = passwordRepeatView.text.toString() // Verify that passwords match if (mMasterPassword != confPassword) { error = true // Passwords do not match - passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match) + passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match) } + } + return error + } - if ((mMasterPassword == null - || mMasterPassword!!.isEmpty()) - && (keyFileCheckBox == null - || !keyFileCheckBox!!.isChecked - || keyFileSelectionView?.uri == null)) { + private fun verifyKeyFile(): Boolean { + var error = false + keyFileSelectionView.error = null + if (keyFileCheckBox.isChecked) { + keyFileSelectionView.uri?.let { uri -> + mKeyFileUri = uri + } ?: run { error = true - showEmptyPasswordConfirmationDialog() + keyFileSelectionView.error = getString(R.string.error_nokeyfile) } } - return error } - private fun verifyKeyFile(): Boolean { + private fun verifyHardwareKey(): Boolean { var error = false - if (keyFileCheckBox != null - && keyFileCheckBox!!.isChecked) { - - keyFileSelectionView?.uri?.let { uri -> - mKeyFile = uri + hardwareKeySelectionView.error = null + if (hardwareKeyCheckBox.isChecked) { + hardwareKeySelectionView.hardwareKey?.let { hardwareKey -> + mHardwareKey = hardwareKey } ?: run { error = true - keyFileSelectionView?.error = getString(R.string.error_nokeyfile) + hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key) } } return error } + private fun retrieveMainCredential(): MainCredential { + val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null + val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null + val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null + return MainCredential(masterPassword, keyFileUri, hardwareKey) + } + + override fun onResume() { + super.onResume() + + // To check checkboxes if a text is present + passwordView.addTextChangedListener(passwordTextWatcher) + } + + override fun onPause() { + super.onPause() + + passwordView.removeTextChangedListener(passwordTextWatcher) + } + private fun showEmptyPasswordConfirmationDialog() { activity?.let { val builder = AlertDialog.Builder(it) builder.setMessage(R.string.warning_empty_password) .setPositiveButton(android.R.string.ok) { _, _ -> - if (!verifyKeyFile()) { - mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) - this@SetMainCredentialDialogFragment.dismiss() - } + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) + this@SetMainCredentialDialogFragment.dismiss() } .setNegativeButton(android.R.string.cancel) { _, _ -> } mEmptyPasswordConfirmationDialog = builder.create() @@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { }) .setPositiveButton(android.R.string.ok) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> - keyFileCheckBox?.isChecked = false - keyFileSelectionView?.uri = null + keyFileCheckBox.isChecked = false + keyFileSelectionView.uri = null } mEmptyKeyFileConfirmationDialog = builder.create() mEmptyKeyFileConfirmationDialog?.show() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/UnderDevelopmentFeatureDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/UnderDevelopmentFeatureDialogFragment.kt index 57fffb0d5..fee8b6476 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/UnderDevelopmentFeatureDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/UnderDevelopmentFeatureDialogFragment.kt @@ -39,6 +39,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(activity) val stringBuilder = SpannableStringBuilder() + /* if (UriUtil.contributingUser(activity)) { stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n") .append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n") @@ -46,14 +47,14 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() { .append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ") builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() } } else { + */ stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n") .append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ") .append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY)) builder.setPositiveButton(R.string.contribute) { _, _ -> UriUtil.gotoUrl(requireContext(), R.string.contribution_url) } - builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() } - } + //} builder.setMessage(stringBuilder) // Create the AlertDialog object and return it return builder.create() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt index cc12c2347..2a4ae25cf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt @@ -56,7 +56,7 @@ class ExternalFileHelper { fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) { - val resultCallback = ActivityResultCallback { result -> + val resultCallback = ActivityResultCallback { result -> result?.let { uri -> UriUtil.takeUriPermission(activity?.contentResolver, uri) onFileSelected?.invoke(uri) @@ -91,7 +91,7 @@ class ExternalFileHelper { fun buildCreateDocument(typeString: String = "application/octet-stream", onFileCreated: (fileCreated: Uri?)->Unit) { - val resultCallback = ActivityResultCallback { result -> + val resultCallback = ActivityResultCallback { result -> onFileCreated.invoke(result) } @@ -150,7 +150,7 @@ class ExternalFileHelper { class OpenDocument : ActivityResultContracts.OpenDocument() { @SuppressLint("InlinedApi") - override fun createIntent(context: Context, input: Array): Intent { + override fun createIntent(context: Context, input: Array): Intent { return super.createIntent(context, input).apply { addCategory(Intent.CATEGORY_OPENABLE) addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) @@ -178,11 +178,10 @@ class ExternalFileHelper { } } - class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() { + class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) { override fun createIntent(context: Context, input: String): Intent { return super.createIntent(context, input).apply { addCategory(Intent.CATEGORY_OPENABLE) - type = typeString } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt index c1d2e72b9..b9be0ffc4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt @@ -6,8 +6,8 @@ import androidx.activity.viewModels import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.viewmodels.DatabaseViewModel @@ -20,7 +20,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mDatabaseTaskProvider = DatabaseTaskProvider(this) + mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog()) mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> val databaseWasReloaded = database?.wasReloaded == true @@ -36,6 +36,17 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { } } + protected open fun showDatabaseDialog(): Boolean { + return true + } + + override fun onDestroy() { + mDatabaseTaskProvider?.destroy() + mDatabaseTaskProvider = null + mDatabase = null + super.onDestroy() + } + override fun onDatabaseRetrieved(database: Database?) { mDatabase = database mDatabaseViewModel.defineDatabase(database) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index 0e55420a7..0f3705ace 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -32,7 +32,6 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.dialogs.DatabaseDialogFragment import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper @@ -44,7 +43,7 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.model.GroupInfo -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable @@ -91,8 +90,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(save) } - mDatabaseViewModel.mergeDatabase.observe(this) { - mDatabaseTaskProvider?.startDatabaseMerge() + mDatabaseViewModel.mergeDatabase.observe(this) { save -> + mDatabaseTaskProvider?.startDatabaseMerge(save) } mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> @@ -228,6 +227,9 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), // Reload the current activity if (result.isSuccess) { reloadActivity() + if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) { + Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show() + } } else { this.showActionErrorIfNeeded(result) finish() @@ -271,11 +273,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } fun mergeDatabase() { - mDatabaseTaskProvider?.startDatabaseMerge() + mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable) } fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) { - mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential) + mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential) } fun reloadDatabase() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt index 32904c342..2b113d03b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt @@ -21,14 +21,21 @@ package com.kunzisoft.keepass.activities.stylish import android.content.ActivityNotFoundException import android.content.Intent +import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log -import android.view.WindowManager +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import com.kunzisoft.keepass.R import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED +import com.kunzisoft.keepass.settings.PreferencesUtil /** * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from @@ -81,8 +88,24 @@ abstract class StylishActivity : AppCompatActivity() { setTheme(themeId) } + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener) + } + private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key -> + if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener + + setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this)) + } + + private fun setScreenshotMode(isEnabled: Boolean) { + findViewById(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE + // Several gingerbread devices have problems with FLAG_SECURE - window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + if (isEnabled) { + window.clearFlags(FLAG_SECURE) + } else { + window.setFlags(FLAG_SECURE, FLAG_SECURE) + } } override fun onResume() { @@ -94,6 +117,7 @@ abstract class StylishActivity : AppCompatActivity() { Log.d(this.javaClass.name, "Theme change detected, restarting activity") recreateActivity() } + setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this)) } private fun recreateActivity() { diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt index 3597ceea4..16d1a190c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/AppDatabase.kt @@ -23,8 +23,15 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import android.content.Context +import androidx.room.AutoMigration -@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class]) +@Database( + version = 2, + entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class], + autoMigrations = [ + AutoMigration (from = 1, to = 2) + ] +) abstract class AppDatabase : RoomDatabase() { abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt index 0785ec45e..cce17c36d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database import android.content.Context import android.net.Uri import android.util.Log +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.SingletonHolderParameter @@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { DatabaseFile( databaseUri, UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri), + HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey), UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""), fileDatabaseInfo.exists, @@ -85,13 +87,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { || !hideBrokenLocations) { databaseFileListLoaded.add( DatabaseFile( - UriUtil.parse(fileDatabaseHistoryEntity.databaseUri), - UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri), - UriUtil.decode(fileDatabaseHistoryEntity.databaseUri), - fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), - fileDatabaseInfo.exists, - fileDatabaseInfo.getLastModificationString(), - fileDatabaseInfo.getSizeString() + UriUtil.parse(fileDatabaseHistoryEntity.databaseUri), + UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri), + HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey), + UriUtil.decode(fileDatabaseHistoryEntity.databaseUri), + fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), + fileDatabaseInfo.exists, + fileDatabaseInfo.getLastModificationString(), + fileDatabaseInfo.getSizeString() ) ) } @@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { ).execute() } - fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null, + fun addOrUpdateDatabaseUri(databaseUri: Uri, + keyFileUri: Uri? = null, + hardwareKey: HardwareKey? = null, databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) { addOrUpdateDatabaseFile(DatabaseFile( - databaseUri, - keyFileUri + databaseUri, + keyFileUri, + hardwareKey ), databaseFileAddedOrUpdatedResult) } @@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { ?: fileDatabaseHistoryRetrieve?.databaseAlias ?: "", databaseFileToAddOrUpdate.keyFileUri?.toString(), + databaseFileToAddOrUpdate.hardwareKey?.value, System.currentTimeMillis() ) @@ -147,13 +154,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistory.databaseUri) DatabaseFile( - UriUtil.parse(fileDatabaseHistory.databaseUri), - UriUtil.parse(fileDatabaseHistory.keyFileUri), - UriUtil.decode(fileDatabaseHistory.databaseUri), - fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), - fileDatabaseInfo.exists, - fileDatabaseInfo.getLastModificationString(), - fileDatabaseInfo.getSizeString() + UriUtil.parse(fileDatabaseHistory.databaseUri), + UriUtil.parse(fileDatabaseHistory.keyFileUri), + HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey), + UriUtil.decode(fileDatabaseHistory.databaseUri), + fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), + fileDatabaseInfo.exists, + fileDatabaseInfo.getLastModificationString(), + fileDatabaseInfo.getSizeString() ) } }, @@ -172,10 +180,11 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory) if (returnValue > 0) { DatabaseFile( - UriUtil.parse(fileDatabaseHistory.databaseUri), - UriUtil.parse(fileDatabaseHistory.keyFileUri), - UriUtil.decode(fileDatabaseHistory.databaseUri), - databaseFileToDelete.databaseAlias + UriUtil.parse(fileDatabaseHistory.databaseUri), + UriUtil.parse(fileDatabaseHistory.keyFileUri), + HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey), + UriUtil.decode(fileDatabaseHistory.databaseUri), + databaseFileToDelete.databaseAlias ) } else { null diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt index 470bec74c..96fd5df08 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryEntity.kt @@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity( @ColumnInfo(name = "keyfile_uri") var keyFileUri: String?, + @ColumnInfo(name = "hardware_key") + var hardwareKey: String?, + @ColumnInfo(name = "updated") val updated: Long ) { diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt index 8a9e200fb..1370deb29 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt @@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope -import com.getkeepsafe.taptargetview.TapTargetView import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.app.database.CipherDatabaseAction -import com.kunzisoft.keepass.database.exception.IODatabaseException -import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException import com.kunzisoft.keepass.model.CipherDecryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CredentialStorage @@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU } } ?: deleteEncryptedDatabaseKey() } - } ?: throw IODatabaseException() + } ?: throw UnknownDatabaseLocationException() } ?: throw Exception("AdvancedUnlockManager not initialized") } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt index f405faeb6..d61d83d69 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignMainCredentialInDatabaseRunnable.kt @@ -24,15 +24,16 @@ import android.net.Uri 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.model.MainCredential -import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.database.element.MainCredential open class AssignMainCredentialInDatabaseRunnable ( - context: Context, - database: Database, - protected val mDatabaseUri: Uri, - protected val mMainCredential: MainCredential) - : SaveDatabaseRunnable(context, database, true) { + context: Context, + database: Database, + protected val mDatabaseUri: Uri, + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) { private var mBackupKey: ByteArray? = null @@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable ( // Set key try { mBackupKey = ByteArray(database.masterKey.size) - System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) - - val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri) - database.assignMasterKey(mMainCredential.masterPassword, uriInputStream) + database.masterKey.copyInto(mBackupKey!!) } catch (e: Exception) { erase(mBackupKey) setError(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index e0c11577c..7707559da 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -24,7 +24,8 @@ import android.net.Uri import android.util.Log import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil class CreateDatabaseRunnable(context: Context, @@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context, private val databaseName: String, private val rootName: String, private val templateGroupName: String?, - mainCredential: MainCredential, + val mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, private val createDatabaseResult: ((Result) -> Unit)?) - : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { + : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) { override fun onStartRun() { try { @@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context, // Add database to recent files if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context.applicationContext) - .addOrUpdateDatabaseUri(mDatabaseUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) + .addOrUpdateDatabaseUri( + mDatabaseUri, + if (PreferencesUtil.rememberKeyFileLocations(context)) mainCredential.keyFileUri else null, + if (PreferencesUtil.rememberHardwareKey(context)) mainCredential.hardwareKey else null, + ) } // Register the current time to init the lock timer @@ -72,6 +77,9 @@ class CreateDatabaseRunnable(context: Context, override fun onFinishRun() { super.onFinishRun() + if (result.isSuccess) { + mDatabase.loaded = true + } createDatabaseResult?.invoke(result) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt index 8430bd816..ebf10bb03 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt @@ -19,7 +19,6 @@ */ package com.kunzisoft.keepass.database.action -import android.app.Service import android.content.* import android.content.Context.* import android.net.Uri @@ -38,14 +37,16 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.model.ProgressMessage import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK @@ -89,11 +90,12 @@ import java.util.* * Utility class to connect an activity or a service to the DatabaseTaskNotificationService, * Useful to retrieve a database instance and sending tasks commands */ -class DatabaseTaskProvider { +class DatabaseTaskProvider(private var context: Context, + private var showDialog: Boolean = true) { - private var activity: FragmentActivity? = null - private var service: Service? = null - private var context: Context + // To show dialog only if context is an activity + private var activity: FragmentActivity? = try { context as? FragmentActivity? } + catch (_: Exception) { null } var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null @@ -101,7 +103,10 @@ class DatabaseTaskProvider { actionTask: String, result: ActionRunnable.Result) -> Unit)? = null - private var intentDatabaseTask: Intent + private var intentDatabaseTask: Intent = Intent( + context.applicationContext, + DatabaseTaskNotificationService::class.java + ) private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null @@ -111,30 +116,33 @@ class DatabaseTaskProvider { private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null - constructor(activity: FragmentActivity) { - this.activity = activity - this.context = activity - this.intentDatabaseTask = Intent(activity.applicationContext, - DatabaseTaskNotificationService::class.java) - } - - constructor(service: Service) { - this.service = service - this.context = service - this.intentDatabaseTask = Intent(service.applicationContext, - DatabaseTaskNotificationService::class.java) + fun destroy() { + this.activity = null + this.onDatabaseRetrieved = null + this.onActionFinish = null + this.databaseTaskBroadcastReceiver = null + this.mBinder = null + this.serviceConnection = null + this.progressTaskDialogFragment = null + this.databaseChangedDialogFragment = null } private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { - override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { - startDialog(titleId, messageId, warningId) + override fun onStartAction(database: Database, + progressMessage: ProgressMessage) { + if (showDialog) + startDialog(progressMessage) } - override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { - updateDialog(titleId, messageId, warningId) + override fun onUpdateAction(database: Database, + progressMessage: ProgressMessage) { + if (showDialog) + updateDialog(progressMessage) } - override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) { + override fun onStopAction(database: Database, + actionTask: String, + result: ActionRunnable.Result) { onActionFinish?.invoke(database, actionTask, result) // Remove the progress task stopDialog() @@ -181,9 +189,7 @@ class DatabaseTaskProvider { } } - private fun startDialog(titleId: Int? = null, - messageId: Int? = null, - warningId: Int? = null) { + private fun startDialog(progressMessage: ProgressMessage) { activity?.let { activity -> activity.lifecycleScope.launch { if (progressTaskDialogFragment == null) { @@ -197,22 +203,17 @@ class DatabaseTaskProvider { PROGRESS_TASK_DIALOG_TAG ) } - updateDialog(titleId, messageId, warningId) + updateDialog(progressMessage) } } } - private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) { + private fun updateDialog(progressMessage: ProgressMessage) { progressTaskDialogFragment?.apply { - titleId?.let { - updateTitle(it) - } - messageId?.let { - updateMessage(it) - } - warningId?.let { - updateWarning(it) - } + updateTitle(progressMessage.titleId) + updateMessage(progressMessage.messageId) + updateWarning(progressMessage.warningId) + setCancellable(progressMessage.cancelable) } } @@ -226,9 +227,7 @@ class DatabaseTaskProvider { serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { - addDatabaseListener(databaseListener) - addDatabaseFileInfoListener(databaseInfoListener) - addActionTaskListener(actionTaskListener) + addServiceListeners(this) getService().checkDatabase() getService().checkDatabaseInfo() getService().checkAction() @@ -236,15 +235,25 @@ class DatabaseTaskProvider { } override fun onServiceDisconnected(name: ComponentName?) { - mBinder?.removeActionTaskListener(actionTaskListener) - mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) - mBinder?.removeDatabaseListener(databaseListener) + removeServiceListeners(mBinder) mBinder = null } } } } + private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { + service?.addDatabaseListener(databaseListener) + service?.addDatabaseFileInfoListener(databaseInfoListener) + service?.addActionTaskListener(actionTaskListener) + } + + private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { + service?.removeActionTaskListener(actionTaskListener) + service?.removeDatabaseFileInfoListener(databaseInfoListener) + service?.removeDatabaseListener(databaseListener) + } + private fun bindService() { initServiceConnection() serviceConnection?.let { @@ -262,10 +271,6 @@ class DatabaseTaskProvider { serviceConnection = null } - fun isBinded(): Boolean { - return mBinder != null - } - fun registerProgressTask() { stopDialog() @@ -299,9 +304,7 @@ class DatabaseTaskProvider { fun unregisterProgressTask() { stopDialog() - mBinder?.removeActionTaskListener(actionTaskListener) - mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) - mBinder?.removeDatabaseListener(databaseListener) + removeServiceListeners(mBinder) mBinder = null unBindService() @@ -321,7 +324,7 @@ class DatabaseTaskProvider { context.startService(intentDatabaseTask) } catch (e: Exception) { Log.e(TAG, "Unable to perform database action", e) - Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show() + Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show() } } @@ -332,7 +335,8 @@ class DatabaseTaskProvider { */ fun startDatabaseCreate(databaseUri: Uri, - mainCredential: MainCredential) { + mainCredential: MainCredential + ) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) @@ -355,9 +359,11 @@ class DatabaseTaskProvider { , ACTION_DATABASE_LOAD_TASK) } - fun startDatabaseMerge(fromDatabaseUri: Uri? = null, + fun startDatabaseMerge(save: Boolean, + fromDatabaseUri: Uri? = null, mainCredential: MainCredential? = null) { start(Bundle().apply { + putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) } @@ -385,7 +391,8 @@ class DatabaseTaskProvider { } fun startDatabaseAssignPassword(databaseUri: Uri, - mainCredential: MainCredential) { + mainCredential: MainCredential + ) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) @@ -702,6 +709,13 @@ class DatabaseTaskProvider { , ACTION_DATABASE_SAVE) } + fun startChallengeResponded(response: ByteArray?) { + start(Bundle().apply { + putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response) + } + , ACTION_CHALLENGE_RESPONDED) + } + companion object { private val TAG = DatabaseTaskProvider::class.java.name } 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 2329ff7f7..24c024721 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 @@ -25,9 +25,10 @@ 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.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.DatabaseInputException +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater @@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, - private val mUri: Uri, + private val mDatabaseUri: Uri, private val mMainCredential: MainCredential, + private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray, private val mReadonly: Boolean, private val mCipherEncryptDatabase: CipherEncryptDatabase?, private val mFixDuplicateUUID: Boolean, @@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context, override fun onActionRun() { try { - mDatabase.loadData(mUri, - mMainCredential, - mReadonly, - context.contentResolver, - UriUtil.getBinaryDir(context), - { memoryWanted -> - BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) - }, - mFixDuplicateUUID, - progressTaskUpdater) + mDatabase.loadData( + context.contentResolver, + mDatabaseUri, + mMainCredential, + mChallengeResponseRetriever, + mReadonly, + UriUtil.getBinaryDir(context), + { memoryWanted -> + BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) + }, + mFixDuplicateUUID, + progressTaskUpdater + ) } - catch (e: LoadDatabaseException) { + catch (e: DatabaseInputException) { setError(e) } @@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context, // Save keyFile in app database if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context) - .addOrUpdateDatabaseUri(mUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) + .addOrUpdateDatabaseUri( + mDatabaseUri, + if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null, + if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null, + ) } // Register the biometric diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt index a0ee19616..2537b4edd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt @@ -22,36 +22,43 @@ 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.element.MainCredential import com.kunzisoft.keepass.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.exception.LoadDatabaseException -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.exception.DatabaseException +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater -class MergeDatabaseRunnable(private val context: Context, - private val mDatabase: Database, - private val mDatabaseToMergeUri: Uri?, - private val mDatabaseToMergeMainCredential: MainCredential?, - private val progressTaskUpdater: ProgressTaskUpdater?, - private val mLoadDatabaseResult: ((Result) -> Unit)?) - : ActionRunnable() { +class MergeDatabaseRunnable( + context: Context, + private val mDatabaseToMergeUri: Uri?, + private val mDatabaseToMergeMainCredential: MainCredential?, + private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, + database: Database, + saveDatabase: Boolean, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, + private val progressTaskUpdater: ProgressTaskUpdater?, + private val mLoadDatabaseResult: ((Result) -> Unit)?) + : SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) { override fun onStartRun() { - mDatabase.wasReloaded = true + database.wasReloaded = true + super.onStartRun() } override fun onActionRun() { try { - mDatabase.mergeData(mDatabaseToMergeUri, - mDatabaseToMergeMainCredential, + database.mergeData( context.contentResolver, + mDatabaseToMergeUri, + mDatabaseToMergeMainCredential, + mDatabaseToMergeChallengeResponseRetriever, { memoryWanted -> BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) }, progressTaskUpdater ) - } catch (e: LoadDatabaseException) { + } catch (e: DatabaseException) { setError(e) } @@ -59,9 +66,11 @@ class MergeDatabaseRunnable(private val context: Context, // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(context) } + super.onActionRun() } override fun onFinishRun() { + super.onFinishRun() mLoadDatabaseResult?.invoke(result) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt index 231c63d87..c382e63e7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.DatabaseException import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater @@ -47,7 +47,7 @@ class ReloadDatabaseRunnable(private val context: Context, BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) }, progressTaskUpdater) - } catch (e: LoadDatabaseException) { + } catch (e: DatabaseException) { setError(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/RemoveUnlinkedDataDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/RemoveUnlinkedDataDatabaseRunnable.kt index 1e3da6cd7..3cc5bdb43 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/RemoveUnlinkedDataDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/RemoveUnlinkedDataDatabaseRunnable.kt @@ -21,12 +21,14 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.hardware.HardwareKey class RemoveUnlinkedDataDatabaseRunnable ( context: Context, database: Database, - saveDatabase: Boolean) - : SaveDatabaseRunnable(context, database, saveDatabase) { + saveDatabase: Boolean, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) { override fun onActionRun() { try { 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 3b88d51ed..c6ae0264a 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 @@ -23,11 +23,15 @@ import android.content.Context import android.net.Uri import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DatabaseException +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable open class SaveDatabaseRunnable(protected var context: Context, protected var database: Database, private var saveDatabase: Boolean, + private var mainCredential: MainCredential?, // If null, uses composite Key + private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, private var databaseCopyUri: Uri? = null) : ActionRunnable() { @@ -39,7 +43,12 @@ open class SaveDatabaseRunnable(protected var context: Context, database.checkVersion() if (saveDatabase && result.isSuccess) { try { - database.saveData(databaseCopyUri, context.contentResolver) + database.saveData( + context.contentResolver, + context.cacheDir, + databaseCopyUri, + mainCredential, + challengeResponseRetriever) } catch (e: DatabaseException) { setError(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt index 5b0e279c4..0c36983d7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/UpdateCompressionBinariesDatabaseRunnable.kt @@ -22,14 +22,16 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm +import com.kunzisoft.keepass.hardware.HardwareKey class UpdateCompressionBinariesDatabaseRunnable ( context: Context, database: Database, private val oldCompressionAlgorithm: CompressionAlgorithm, private val newCompressionAlgorithm: CompressionAlgorithm, - saveDatabase: Boolean) - : SaveDatabaseRunnable(context, database, saveDatabase) { + saveDatabase: Boolean, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) { override fun onStartRun() { // Set new compression diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/history/DeleteEntryHistoryDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/history/DeleteEntryHistoryDatabaseRunnable.kt index 2e6c2eab0..58a1ad6ad 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/history/DeleteEntryHistoryDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/history/DeleteEntryHistoryDatabaseRunnable.kt @@ -23,14 +23,16 @@ import android.content.Context import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.hardware.HardwareKey class DeleteEntryHistoryDatabaseRunnable ( context: Context, database: Database, private val mainEntry: Entry, private val entryHistoryPosition: Int, - saveDatabase: Boolean) - : SaveDatabaseRunnable(context, database, saveDatabase) { + saveDatabase: Boolean, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) { override fun onStartRun() { try { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/history/RestoreEntryHistoryDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/history/RestoreEntryHistoryDatabaseRunnable.kt index 7163cdf38..6b97d8217 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/history/RestoreEntryHistoryDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/history/RestoreEntryHistoryDatabaseRunnable.kt @@ -23,6 +23,7 @@ import android.content.Context import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.tasks.ActionRunnable class RestoreEntryHistoryDatabaseRunnable ( @@ -30,7 +31,8 @@ class RestoreEntryHistoryDatabaseRunnable ( private val database: Database, private val mainEntry: Entry, private val entryHistoryPosition: Int, - private val saveDatabase: Boolean) + private val saveDatabase: Boolean, + private val challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) : ActionRunnable() { private var updateEntryRunnable: UpdateEntryRunnable? = null @@ -43,12 +45,15 @@ class RestoreEntryHistoryDatabaseRunnable ( historyToRestore.addEntryToHistory(it) } // Update the entry with the fresh formatted entry to restore - updateEntryRunnable = UpdateEntryRunnable(context, - database, - mainEntry, - historyToRestore, - saveDatabase, - null) + updateEntryRunnable = UpdateEntryRunnable( + context, + database, + mainEntry, + historyToRestore, + saveDatabase, + null, + challengeResponseRetriever + ) updateEntryRunnable?.onStartRun() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt index 2feeb82fc..717c62b6e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/ActionNodeDatabaseRunnable.kt @@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node import android.content.Context import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.hardware.HardwareKey abstract class ActionNodeDatabaseRunnable( context: Context, database: Database, private val afterActionNodesFinish: AfterActionNodesFinish?, - save: Boolean) - : SaveDatabaseRunnable(context, database, save) { + save: Boolean, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) { /** * Function do to a node action diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt index ecfd392c6..1178051cc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddEntryRunnable.kt @@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.hardware.HardwareKey class AddEntryRunnable constructor( context: Context, @@ -31,8 +32,9 @@ class AddEntryRunnable constructor( private val mNewEntry: Entry, private val mParent: Group, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish?) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { override fun nodeAction() { mNewEntry.touch(modified = true, touchParents = true) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt index 5c3285e4c..12607f9eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/AddGroupRunnable.kt @@ -23,6 +23,7 @@ import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.hardware.HardwareKey class AddGroupRunnable constructor( context: Context, @@ -30,8 +31,9 @@ class AddGroupRunnable constructor( private val mNewGroup: Group, private val mParent: Group, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish?) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { override fun nodeAction() { mNewGroup.touch(modified = true, touchParents = true) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt index dbdaba94e..792643a67 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/CopyNodesRunnable.kt @@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node import android.content.Context import android.util.Log -import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException +import com.kunzisoft.keepass.hardware.HardwareKey class CopyNodesRunnable constructor( context: Context, @@ -33,8 +36,9 @@ class CopyNodesRunnable constructor( private val mNodesToCopy: List, private val mNewParent: Group, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish?) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { private var mEntriesCopied = ArrayList() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt index 2175a6b08..0aca84eac 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt @@ -20,16 +20,20 @@ package com.kunzisoft.keepass.database.action.node import android.content.Context -import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.hardware.HardwareKey class DeleteNodesRunnable(context: Context, database: Database, private val mNodesToDelete: List, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { private var mOldParent: Group? = null private var mCanRecycle: Boolean = false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt index 17ad24fa3..2e2991e15 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt @@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node import android.content.Context import android.util.Log -import com.kunzisoft.keepass.database.element.* +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException +import com.kunzisoft.keepass.hardware.HardwareKey class MoveNodesRunnable constructor( context: Context, @@ -33,8 +36,9 @@ class MoveNodesRunnable constructor( private val mNodesToMove: List, private val mNewParent: Group, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish?) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { private var mOldParent: Group? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt index c1e781248..41f7bafe4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateEntryRunnable.kt @@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.hardware.HardwareKey class UpdateEntryRunnable constructor( context: Context, @@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor( private val mOldEntry: Entry, private val mNewEntry: Entry, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish?) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { override fun nodeAction() { if (mOldEntry.nodeId == mNewEntry.nodeId) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt index 79e5f635b..53de6a6aa 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/UpdateGroupRunnable.kt @@ -23,6 +23,7 @@ import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.hardware.HardwareKey class UpdateGroupRunnable constructor( context: Context, @@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor( private val mOldGroup: Group, private val mNewGroup: Group, save: Boolean, - afterActionNodesFinish: AfterActionNodesFinish?) - : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { + afterActionNodesFinish: AfterActionNodesFinish?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) { override fun nodeAction() { if (mOldGroup.nodeId == mNewGroup.nodeId) { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt new file mode 100644 index 000000000..6423068f9 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt @@ -0,0 +1,34 @@ +package com.kunzisoft.keepass.database.element + +import com.kunzisoft.keepass.hardware.HardwareKey + +data class CompositeKey(var passwordData: ByteArray? = null, + var keyFileData: ByteArray? = null, + var hardwareKey: HardwareKey? = null) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CompositeKey + + if (passwordData != null) { + if (other.passwordData == null) return false + if (!passwordData.contentEquals(other.passwordData)) return false + } else if (other.passwordData != null) return false + if (keyFileData != null) { + if (other.keyFileData == null) return false + if (!keyFileData.contentEquals(other.keyFileData)) return false + } else if (other.keyFileData != null) return false + if (hardwareKey != other.hardwareKey) return false + + return true + } + + override fun hashCode(): Int { + var result = passwordData?.contentHashCode() ?: 0 + result = 31 * result + (keyFileData?.contentHashCode() ?: 0) + result = 31 * result + (hardwareKey?.hashCode() ?: 0) + return result + } +} \ No newline at end of file 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 189a14dbf..a2d594d8d 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 @@ -54,12 +54,10 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchParameters +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.icons.IconDrawableFactory -import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.tasks.ProgressTaskUpdater -import com.kunzisoft.keepass.utils.SingletonHolder -import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.readBytes4ToUInt +import com.kunzisoft.keepass.utils.* import java.io.* import java.util.* @@ -73,7 +71,7 @@ class Database { var fileUri: Uri? = null private set - private var mSearchHelper: SearchHelper? = null + private var mSearchHelper: SearchHelper = SearchHelper() var isReadOnly = false @@ -384,10 +382,14 @@ class Database { set(masterKey) { mDatabaseKDB?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey + mDatabaseKDBX?.keyLastChanged = DateInstant() mDatabaseKDBX?.settingsChanged = DateInstant() dataModifiedSinceLastLoading = true } + val transformSeed: ByteArray? + get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed + var rootGroup: Group? get() { mDatabaseKDB?.rootGroup?.let { @@ -553,83 +555,31 @@ class Database { setDatabaseKDBX(newDatabase) this.fileUri = databaseUri // Set Database state - this.loaded = true this.dataModifiedSinceLastLoading = false } - @Throws(LoadDatabaseException::class) - private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, - openDatabaseKDB: (InputStream) -> DatabaseKDB, - openDatabaseKDBX: (InputStream) -> DatabaseKDBX) { - var databaseInputStream: InputStream? = null - try { - // Load Data, pass Uris as InputStreams - val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) - ?: throw IOException("Database input stream cannot be retrieve") - - databaseInputStream = BufferedInputStream(databaseStream) - if (!databaseInputStream.markSupported()) { - throw IOException("Input stream does not support mark.") - } - - // We'll end up reading 8 bytes to identify the header. Might as well use two extra. - databaseInputStream.mark(10) - - // Get the file directory to save the attachments - val sig1 = databaseInputStream.readBytes4ToUInt() - val sig2 = databaseInputStream.readBytes4ToUInt() - - // Return to the start - databaseInputStream.reset() - - when { - // Header of database KDB - DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream)) - - // Header of database KDBX - DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream)) - - // Header not recognized - else -> throw SignatureDatabaseException() - } - - this.mSearchHelper = SearchHelper() - loaded = true - } catch (e: LoadDatabaseException) { - throw e - } catch (e: Exception) { - throw LoadDatabaseException(e) - } finally { - databaseInputStream?.close() - } - } - - @Throws(LoadDatabaseException::class) - fun loadData(uri: Uri, - mainCredential: MainCredential, - readOnly: Boolean, - contentResolver: ContentResolver, - cacheDirectory: File, - isRAMSufficient: (memoryWanted: Long) -> Boolean, - fixDuplicateUUID: Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { + @Throws(DatabaseInputException::class) + fun loadData( + contentResolver: ContentResolver, + databaseUri: Uri, + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, + readOnly: Boolean, + cacheDirectory: File, + isRAMSufficient: (memoryWanted: Long) -> Boolean, + fixDuplicateUUID: Boolean, + progressTaskUpdater: ProgressTaskUpdater? + ) { // Save database URI - this.fileUri = uri + this.fileUri = databaseUri // Check if the file is writable this.isReadOnly = readOnly - // Pass KeyFile Uri as InputStreams - var keyFileInputStream: InputStream? = null try { - // Get keyFile inputStream - mainCredential.keyFileUri?.let { keyFile -> - keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile) - } - // Read database stream for the first time - readDatabaseStream(contentResolver, uri, + readDatabaseStream(contentResolver, databaseUri, { databaseInputStream -> val databaseKDB = DatabaseKDB().apply { binaryCache.cacheDirectory = cacheDirectory @@ -639,12 +589,12 @@ class Database { .openDatabase(databaseInputStream, progressTaskUpdater ) { - databaseKDB.retrieveMasterKey( - mainCredential.masterPassword, - keyFileInputStream + databaseKDB.deriveMasterKey( + contentResolver, + mainCredential ) } - databaseKDB + setDatabaseKDB(databaseKDB) }, { databaseInputStream -> val databaseKDBX = DatabaseKDBX().apply { @@ -655,23 +605,23 @@ class Database { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { - databaseKDBX.retrieveMasterKey( - mainCredential.masterPassword, - keyFileInputStream, + databaseKDBX.deriveMasterKey( + contentResolver, + mainCredential, + challengeResponseRetriever ) } } - databaseKDBX + setDatabaseKDBX(databaseKDBX) } ) - } catch (e: FileNotFoundException) { - throw FileNotFoundDatabaseException("Unable to load the keyfile") - } catch (e: LoadDatabaseException) { - throw e + loaded = true } catch (e: Exception) { - throw LoadDatabaseException(e) + Log.e(TAG, "Unable to load the database") + if (e is DatabaseInputException) + throw e + throw DatabaseInputException(e) } finally { - keyFileInputStream?.close() dataModifiedSinceLastLoading = false } } @@ -680,48 +630,44 @@ class Database { return mDatabaseKDBX != null } - @Throws(LoadDatabaseException::class) - fun mergeData(databaseToMergeUri: Uri?, - databaseToMergeMainCredential: MainCredential?, - contentResolver: ContentResolver, - isRAMSufficient: (memoryWanted: Long) -> Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { + @Throws(DatabaseInputException::class) + fun mergeData( + contentResolver: ContentResolver, + databaseToMergeUri: Uri?, + databaseToMergeMainCredential: MainCredential?, + databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, + isRAMSufficient: (memoryWanted: Long) -> Boolean, + progressTaskUpdater: ProgressTaskUpdater? + ) { mDatabaseKDB?.let { - throw IODatabaseException("Unable to merge from a database V1") + throw MergeDatabaseKDBException() } // New database instance to get new changes val databaseToMerge = Database() databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri - // Pass KeyFile Uri as InputStreams - var keyFileInputStream: InputStream? = null try { val databaseUri = databaseToMerge.fileUri if (databaseUri != null) { - if (databaseToMergeMainCredential != null) { - // Get keyFile inputStream - databaseToMergeMainCredential.keyFileUri?.let { keyFile -> - keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile) - } - } - - databaseToMerge.readDatabaseStream(contentResolver, databaseUri, + readDatabaseStream(contentResolver, databaseUri, { databaseInputStream -> val databaseToMergeKDB = DatabaseKDB() DatabaseInputKDB(databaseToMergeKDB) .openDatabase(databaseInputStream, progressTaskUpdater) { if (databaseToMergeMainCredential != null) { - databaseToMergeKDB.retrieveMasterKey( - databaseToMergeMainCredential.masterPassword, - keyFileInputStream, + databaseToMergeKDB.deriveMasterKey( + contentResolver, + databaseToMergeMainCredential ) } else { - databaseToMergeKDB.masterKey = masterKey + this@Database.mDatabaseKDB?.let { thisDatabaseKDB -> + databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB) + } } } - databaseToMergeKDB + databaseToMerge.setDatabaseKDB(databaseToMergeKDB) }, { databaseInputStream -> val databaseToMergeKDBX = DatabaseKDBX() @@ -729,18 +675,22 @@ class Database { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { if (databaseToMergeMainCredential != null) { - databaseToMergeKDBX.retrieveMasterKey( - databaseToMergeMainCredential.masterPassword, - keyFileInputStream, + databaseToMergeKDBX.deriveMasterKey( + contentResolver, + databaseToMergeMainCredential, + databaseToMergeChallengeResponseRetriever ) } else { - databaseToMergeKDBX.masterKey = masterKey + this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX -> + databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX) + } } } } - databaseToMergeKDBX + databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX) } ) + loaded = true mDatabaseKDBX?.let { currentDatabaseKDBX -> val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply { @@ -760,24 +710,24 @@ class Database { } } } else { - throw IODatabaseException("Database URI is null, database cannot be merged") + throw UnknownDatabaseLocationException() } - } catch (e: FileNotFoundException) { - throw FileNotFoundDatabaseException("Unable to load the keyfile") - } catch (e: LoadDatabaseException) { - throw e } catch (e: Exception) { - throw LoadDatabaseException(e) + Log.e(TAG, "Unable to merge the database") + if (e is DatabaseException) + throw e + throw DatabaseInputException(e) } finally { - keyFileInputStream?.close() databaseToMerge.clearAndClose() } } - @Throws(LoadDatabaseException::class) - fun reloadData(contentResolver: ContentResolver, - isRAMSufficient: (memoryWanted: Long) -> Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { + @Throws(DatabaseInputException::class) + fun reloadData( + contentResolver: ContentResolver, + isRAMSufficient: (memoryWanted: Long) -> Boolean, + progressTaskUpdater: ProgressTaskUpdater? + ) { // Retrieve the stream from the old database URI try { @@ -791,9 +741,11 @@ class Database { } DatabaseInputKDB(databaseKDB) .openDatabase(databaseInputStream, progressTaskUpdater) { - databaseKDB.masterKey = masterKey + this@Database.mDatabaseKDB?.let { thisDatabaseKDB -> + databaseKDB.copyMasterKeyFrom(thisDatabaseKDB) + } } - databaseKDB + setDatabaseKDB(databaseKDB) }, { databaseInputStream -> val databaseKDBX = DatabaseKDBX() @@ -803,26 +755,144 @@ class Database { DatabaseInputKDBX(databaseKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { - databaseKDBX.masterKey = masterKey + this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX -> + databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX) + } } } - databaseKDBX + setDatabaseKDBX(databaseKDBX) } ) + loaded = true } else { - throw IODatabaseException("Database URI is null, database cannot be reloaded") + throw UnknownDatabaseLocationException() } - } catch (e: FileNotFoundException) { - throw FileNotFoundDatabaseException("Unable to load the keyfile") - } catch (e: LoadDatabaseException) { - throw e } catch (e: Exception) { - throw LoadDatabaseException(e) + Log.e(TAG, "Unable to reload the database") + if (e is DatabaseException) + throw e + throw DatabaseInputException(e) } finally { dataModifiedSinceLastLoading = false } } + @Throws(Exception::class) + private fun readDatabaseStream(contentResolver: ContentResolver, + databaseUri: Uri, + openDatabaseKDB: (InputStream) -> Unit, + openDatabaseKDBX: (InputStream) -> Unit) { + try { + // Load Data, pass Uris as InputStreams + val databaseStream = UriUtil.getUriInputStream(contentResolver, databaseUri) + ?: throw UnknownDatabaseLocationException() + + BufferedInputStream(databaseStream).use { databaseInputStream -> + + // We'll end up reading 8 bytes to identify the header. Might as well use two extra. + databaseInputStream.mark(10) + + // Get the file directory to save the attachments + val sig1 = databaseInputStream.readBytes4ToUInt() + val sig2 = databaseInputStream.readBytes4ToUInt() + + // Return to the start + databaseInputStream.reset() + + when { + // Header of database KDB + DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> openDatabaseKDB( + databaseInputStream + ) + // Header of database KDBX + DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> openDatabaseKDBX( + databaseInputStream + ) + // Header not recognized + else -> throw SignatureDatabaseException() + } + } + } catch (fileNotFoundException : FileNotFoundException) { + throw FileNotFoundDatabaseException() + } + } + + @Throws(DatabaseOutputException::class) + fun saveData(contentResolver: ContentResolver, + cacheDir: File, + databaseCopyUri: Uri?, + mainCredential: MainCredential?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) { + val saveUri = databaseCopyUri ?: this.fileUri + // Build temp database file to avoid file corruption if error + val cacheFile = File(cacheDir, saveUri.hashCode().toString()) + try { + if (saveUri != null) { + // Save in a temp memory to avoid exception + cacheFile.outputStream().use { outputStream -> + mDatabaseKDB?.let { databaseKDB -> + DatabaseOutputKDB(databaseKDB).apply { + writeDatabase(outputStream) { + if (mainCredential != null) { + databaseKDB.deriveMasterKey( + contentResolver, + mainCredential + ) + } else { + // No master key change + } + } + } + } + ?: mDatabaseKDBX?.let { databaseKDBX -> + DatabaseOutputKDBX(databaseKDBX).apply { + writeDatabase(outputStream) { + if (mainCredential != null) { + // Build new master key from MainCredential + databaseKDBX.deriveMasterKey( + contentResolver, + mainCredential, + challengeResponseRetriever + ) + } else { + // Reuse composite key parts + databaseKDBX.deriveCompositeKey( + challengeResponseRetriever + ) + } + } + } + } + } + // Copy from the cache to the final stream + UriUtil.getUriOutputStream(contentResolver, saveUri)?.use { outputStream -> + cacheFile.inputStream().use { inputStream -> + inputStream.readAllBytes { buffer -> + outputStream.write(buffer) + } + } + } + } else { + throw UnknownDatabaseLocationException() + } + } catch (e: Exception) { + Log.e(TAG, "Unable to save database", e) + if (e is DatabaseException) + throw e + throw DatabaseOutputException(e) + } finally { + try { + Log.d(TAG, "Delete database cache file $cacheFile") + cacheFile.delete() + } catch (e: Exception) { + Log.e(TAG, "Cache file $cacheFile cannot be deleted", e) + } + if (databaseCopyUri == null) { + this.dataModifiedSinceLastLoading = false + } + } + } + fun groupIsInRecycleBin(group: Group): Boolean { val groupKDB = group.groupKDB val groupKDBX = group.groupKDBX @@ -845,13 +915,13 @@ class Database { fun createVirtualGroupFromSearch(searchParameters: SearchParameters, fromGroup: NodeId<*>? = null, max: Int = Integer.MAX_VALUE): Group? { - return mSearchHelper?.createVirtualGroupWithSearchResult(this, + return mSearchHelper.createVirtualGroupWithSearchResult(this, searchParameters, fromGroup, max) } fun createVirtualGroupFromSearchInfo(searchInfoString: String, max: Int = Integer.MAX_VALUE): Group? { - return mSearchHelper?.createVirtualGroupWithSearchResult(this, + return mSearchHelper.createVirtualGroupWithSearchResult(this, SearchParameters().apply { searchQuery = searchInfoString searchInTitles = true @@ -908,40 +978,6 @@ class Database { dataModifiedSinceLastLoading = true } - @Throws(DatabaseOutputException::class) - fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) { - try { - val saveUri = databaseCopyUri ?: this.fileUri - if (saveUri != null) { - var outputStream: OutputStream? = null - try { - outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri) - outputStream?.let { definedOutputStream -> - val databaseOutput = - mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) } - ?: mDatabaseKDBX?.let { - DatabaseOutputKDBX( - it, - definedOutputStream - ) - } - databaseOutput?.output() - } - } catch (e: Exception) { - throw IOException(e) - } finally { - outputStream?.close() - } - if (databaseCopyUri == null) { - this.dataModifiedSinceLastLoading = false - } - } - } catch (e: Exception) { - Log.e(TAG, "Unable to save database", e) - throw DatabaseOutputException(e) - } - } - fun clearIndexesAndBinaries(filesDirectory: File? = null) { this.mDatabaseKDB?.clearIndexes() this.mDatabaseKDBX?.clearIndexes() @@ -987,20 +1023,13 @@ class Database { } fun validatePasswordEncoding(mainCredential: MainCredential): Boolean { - val password = mainCredential.masterPassword + val password = mainCredential.password val containsKeyFile = mainCredential.keyFileUri != null return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile) ?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile) ?: false } - @Throws(IOException::class) - fun assignMasterKey(key: String?, keyInputStream: InputStream?) { - mDatabaseKDB?.retrieveMasterKey(key, keyInputStream) - mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream) - mDatabaseKDBX?.keyLastChanged = DateInstant() - } - fun rootCanContainsEntry(): Boolean { return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/MainCredential.kt new file mode 100644 index 000000000..172c228ec --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/MainCredential.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2022 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + */ +package com.kunzisoft.keepass.database.element + +import android.content.ContentResolver +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.util.Base64 +import android.util.Log +import com.kunzisoft.encrypt.HashManager +import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars +import com.kunzisoft.keepass.utils.StringUtil.toHexString +import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum +import org.apache.commons.codec.binary.Hex +import org.w3c.dom.Node +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.io.UnsupportedEncodingException +import java.nio.charset.Charset +import javax.xml.XMLConstants +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.parsers.ParserConfigurationException + +data class MainCredential(var password: String? = null, + var keyFileUri: Uri? = null, + var hardwareKey: HardwareKey? = null): Parcelable { + + constructor(parcel: Parcel) : this() { + password = parcel.readString() + keyFileUri = parcel.readParcelable(Uri::class.java.classLoader) + hardwareKey = parcel.readEnum() + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(password) + parcel.writeParcelable(keyFileUri, flags) + parcel.writeEnum(hardwareKey) + } + + override fun describeContents(): Int { + return 0 + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MainCredential + + if (password != other.password) return false + if (keyFileUri != other.keyFileUri) return false + if (hardwareKey != other.hardwareKey) return false + + return true + } + + override fun hashCode(): Int { + var result = password?.hashCode() ?: 0 + result = 31 * result + (keyFileUri?.hashCode() ?: 0) + result = 31 * result + (hardwareKey?.hashCode() ?: 0) + return result + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MainCredential { + return MainCredential(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + private val TAG = MainCredential::class.java.simpleName + + @Throws(IOException::class) + fun retrievePasswordKey(key: String, + encoding: Charset + ): ByteArray { + val bKey: ByteArray = try { + key.toByteArray(encoding) + } catch (e: UnsupportedEncodingException) { + key.toByteArray() + } + return HashManager.hashSha256(bKey) + } + + @Throws(IOException::class) + fun retrieveFileKey(contentResolver: ContentResolver, + keyFileUri: Uri?, + allowXML: Boolean): ByteArray { + if (keyFileUri == null) + throw IOException("Keyfile URI is null") + val keyData = getKeyFileData(contentResolver, keyFileUri) + ?: throw IOException("No data retrieved") + try { + // Check XML key file + val xmlKeyByteArray = if (allowXML) + loadXmlKeyFile(ByteArrayInputStream(keyData)) + else + null + if (xmlKeyByteArray != null) { + return xmlKeyByteArray + } + + // Check 32 bytes key file + when (keyData.size) { + 32 -> return keyData + 64 -> try { + return Hex.decodeHex(String(keyData).toCharArray()) + } catch (ignoredException: Exception) { + // Key is not base 64, treat it as binary data + } + } + // Hash file as binary data + return HashManager.hashSha256(keyData) + } catch (e: Exception) { + throw IOException("Unable to load the keyfile.", e) + } + } + + @Throws(IOException::class) + fun retrieveHardwareKey(keyData: ByteArray): ByteArray { + return HashManager.hashSha256(keyData) + } + + @Throws(Exception::class) + private fun getKeyFileData(contentResolver: ContentResolver, + keyFileUri: Uri): ByteArray? { + UriUtil.getUriInputStream(contentResolver, keyFileUri)?.use { keyFileInputStream -> + return keyFileInputStream.readBytes() + } + return null + } + + private fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { + try { + val documentBuilderFactory = DocumentBuilderFactory.newInstance() + + // Disable certain unsecure XML-Parsing DocumentBuilderFactory features + try { + documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + } catch (e : ParserConfigurationException) { + Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)") + } + + val documentBuilder = documentBuilderFactory.newDocumentBuilder() + val doc = documentBuilder.parse(keyInputStream) + + var xmlKeyFileVersion = 1F + + val docElement = doc.documentElement + val keyFileChildNodes = docElement.childNodes + // Root node + if (docElement == null + || !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) { + return null + } + if (keyFileChildNodes.length < 2) + return null + for (keyFileChildPosition in 0 until keyFileChildNodes.length) { + val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition) + // + if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) { + val metaChildNodes = keyFileChildNode.childNodes + for (metaChildPosition in 0 until metaChildNodes.length) { + val metaChildNode = metaChildNodes.item(metaChildPosition) + // + if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) { + val versionChildNodes = metaChildNode.childNodes + for (versionChildPosition in 0 until versionChildNodes.length) { + val versionChildNode = versionChildNodes.item(versionChildPosition) + if (versionChildNode.nodeType == Node.TEXT_NODE) { + val versionText = versionChildNode.textContent.removeSpaceChars() + try { + xmlKeyFileVersion = versionText.toFloat() + Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion") + } catch (e: Exception) { + Log.e(TAG, "XML Keyfile version cannot be read : $versionText") + } + } + } + } + } + } + // + if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) { + val keyChildNodes = keyFileChildNode.childNodes + for (keyChildPosition in 0 until keyChildNodes.length) { + val keyChildNode = keyChildNodes.item(keyChildPosition) + // + if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) { + var hashString : String? = null + if (keyChildNode.hasAttributes()) { + val dataNodeAttributes = keyChildNode.attributes + hashString = dataNodeAttributes + .getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue + } + val dataChildNodes = keyChildNode.childNodes + for (dataChildPosition in 0 until dataChildNodes.length) { + val dataChildNode = dataChildNodes.item(dataChildPosition) + if (dataChildNode.nodeType == Node.TEXT_NODE) { + val dataString = dataChildNode.textContent.removeSpaceChars() + when (xmlKeyFileVersion) { + 1F -> { + // No hash in KeyFile XML version 1 + return Base64.decode(dataString, + DatabaseKDBX.BASE_64_FLAG + ) + } + 2F -> { + return if (hashString != null + && checkKeyFileHash(dataString, hashString) + ) { + Log.i(TAG, "Successful key file hash check.") + Hex.decodeHex(dataString.toCharArray()) + } else { + Log.e(TAG, "Unable to check the hash of the key file.") + null + } + } + } + } + } + } + } + } + } + } catch (e: Exception) { + return null + } + return null + } + + private fun checkKeyFileHash(data: String, hash: String): Boolean { + var success = false + try { + // hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key. + val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray())) + .copyOfRange(0, 4).toHexString() + success = dataDigest == hash + } catch (e: Exception) { + e.printStackTrace() + } + return success + } + + private const val XML_NODE_ROOT_NAME = "KeyFile" + private const val XML_NODE_META_NAME = "Meta" + private const val XML_NODE_VERSION_NAME = "Version" + private const val XML_NODE_KEY_NAME = "Key" + private const val XML_NODE_DATA_NAME = "Data" + private const val XML_ATTRIBUTE_DATA_HASH = "Hash" + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt index f10901014..b98826cb3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt @@ -19,11 +19,13 @@ package com.kunzisoft.keepass.database.element.database +import android.content.ContentResolver import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.aes.AESTransformer import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.group.GroupKDB @@ -31,8 +33,10 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeVersioned +import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException +import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException import java.io.IOException -import java.io.InputStream +import java.nio.charset.Charset import java.util.* class DatabaseKDB : DatabaseVersioned() { @@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned() { KdfFactory.aesKdf ) - override val passwordEncoding: String - get() = "ISO-8859-1" + override val passwordEncoding: Charset + get() = Charsets.ISO_8859_1 override var numberKeyEncryptionRounds = 300L @@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned() { return newId } - @Throws(IOException::class) - override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { - - return if (key != null && keyInputStream != null) { - getCompositeKey(key, keyInputStream) - } else if (key != null) { // key.length() >= 0 - getPasswordKey(key) - } else if (keyInputStream != null) { // key == null - getFileKey(keyInputStream) - } else { - throw IllegalArgumentException("Key cannot be empty.") - } - } - @Throws(IOException::class) fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) { // Encrypt the master key a few times to make brute-force key-search harder @@ -138,6 +128,41 @@ class DatabaseKDB : DatabaseVersioned() { finalKey = HashManager.hashSha256(masterSeed, transformedKey) } + fun deriveMasterKey( + contentResolver: ContentResolver, + mainCredential: MainCredential + ) { + // Exception when no password + if (mainCredential.hardwareKey != null) + throw HardwareKeyDatabaseException() + if (mainCredential.password == null && mainCredential.keyFileUri == null) + throw EmptyKeyDatabaseException() + + // Retrieve plain data + val password = mainCredential.password + val keyFileUri = mainCredential.keyFileUri + val passwordBytes = if (password != null) MainCredential.retrievePasswordKey( + password, + passwordEncoding + ) else null + val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey( + contentResolver, + keyFileUri, + false + ) else null + + // Build master key + if (passwordBytes != null + && keyFileBytes != null) { + this.masterKey = HashManager.hashSha256( + passwordBytes, + keyFileBytes + ) + } else { + this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0) + } + } + override fun createGroup(): GroupKDB { return GroupKDB() } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index 6d1602409..5440faf64 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.database.element.database +import android.content.ContentResolver import android.content.res.Resources import android.util.Base64 import android.util.Log @@ -31,10 +32,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.AesKdf import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters -import com.kunzisoft.keepass.database.element.CustomData -import com.kunzisoft.keepass.database.element.DateInstant -import com.kunzisoft.keepass.database.element.DeletedObject -import com.kunzisoft.keepass.database.element.Tags +import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.element.entry.EntryKDBX @@ -49,30 +47,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible +import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 -import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars -import com.kunzisoft.keepass.utils.StringUtil.toHexString +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.longTo8Bytes -import org.apache.commons.codec.binary.Hex -import org.w3c.dom.Node import java.io.IOException -import java.io.InputStream +import java.nio.charset.Charset import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* import javax.crypto.Mac -import javax.xml.XMLConstants -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.parsers.ParserConfigurationException -import kotlin.collections.HashSet import kotlin.math.min class DatabaseKDBX : DatabaseVersioned { + // To resave the database with same credential when already loaded + private var mCompositeKey = CompositeKey() + var hmacKey: ByteArray? = null private set @@ -233,6 +228,79 @@ class DatabaseKDBX : DatabaseVersioned { } } + fun deriveMasterKey( + contentResolver: ContentResolver, + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ) { + // Retrieve each plain credential + val password = mainCredential.password + val keyFileUri = mainCredential.keyFileUri + val hardwareKey = mainCredential.hardwareKey + val passwordBytes = if (password != null) MainCredential.retrievePasswordKey( + password, + passwordEncoding + ) else null + val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey( + contentResolver, + keyFileUri, + true + ) else null + val hardwareKeyBytes = if (hardwareKey != null) MainCredential.retrieveHardwareKey( + challengeResponseRetriever.invoke(hardwareKey, transformSeed) + ) else null + + // Save to rebuild master password with new seed later + mCompositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKey) + + // Build the master key + this.masterKey = composedKeyToMasterKey( + passwordBytes, + keyFileBytes, + hardwareKeyBytes + ) + } + + @Throws(DatabaseOutputException::class) + fun deriveCompositeKey( + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ) { + val passwordBytes = mCompositeKey.passwordData + val keyFileBytes = mCompositeKey.keyFileData + val hardwareKey = mCompositeKey.hardwareKey + if (hardwareKey == null) { + // If no hardware key, simply rebuild from composed keys + this.masterKey = composedKeyToMasterKey( + passwordBytes, + keyFileBytes + ) + } else { + val hardwareKeyBytes = MainCredential.retrieveHardwareKey( + challengeResponseRetriever.invoke(hardwareKey, transformSeed) + ) + this.masterKey = composedKeyToMasterKey( + passwordBytes, + keyFileBytes, + hardwareKeyBytes + ) + } + } + + private fun composedKeyToMasterKey(passwordData: ByteArray?, + keyFileData: ByteArray?, + hardwareKeyData: ByteArray? = null): ByteArray { + return HashManager.hashSha256( + passwordData, + keyFileData, + hardwareKeyData + ) + } + + fun copyMasterKeyFrom(databaseVersioned: DatabaseKDBX) { + super.copyMasterKeyFrom(databaseVersioned) + this.mCompositeKey = databaseVersioned.mCompositeKey + } + fun getMinKdbxVersion(): UnsignedInt { val entryHandler = EntryOperationHandler() val groupHandler = GroupOperationHandler() @@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned { kdfEngine.setParallelism(kdfParameters!!, parallelism) } - override val passwordEncoding: String - get() = "UTF-8" + override val passwordEncoding: Charset + get() = Charsets.UTF_8 private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? { if (groupUUID == UUID_ZERO) @@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned { return mFieldReferenceEngine.compile(textReference, recursionLevel) } - @Throws(IOException::class) - public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { - - var masterKey = byteArrayOf() - - if (key != null && keyInputStream != null) { - return getCompositeKey(key, keyInputStream) - } else if (key != null) { // key.length() >= 0 - masterKey = getPasswordKey(key) - } else if (keyInputStream != null) { // key == null - masterKey = getFileKey(keyInputStream) - } - - return HashManager.hashSha256(masterKey) - } - @Throws(IOException::class) fun makeFinalKey(masterSeed: ByteArray) { @@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned { return ret } - override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { - try { - val documentBuilderFactory = DocumentBuilderFactory.newInstance() - - // Disable certain unsecure XML-Parsing DocumentBuilderFactory features - try { - documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) - } catch (e : ParserConfigurationException) { - Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)") - } - - val documentBuilder = documentBuilderFactory.newDocumentBuilder() - val doc = documentBuilder.parse(keyInputStream) - - var xmlKeyFileVersion = 1F - - val docElement = doc.documentElement - val keyFileChildNodes = docElement.childNodes - // Root node - if (docElement == null - || !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) { - return null - } - if (keyFileChildNodes.length < 2) - return null - for (keyFileChildPosition in 0 until keyFileChildNodes.length) { - val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition) - // - if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) { - val metaChildNodes = keyFileChildNode.childNodes - for (metaChildPosition in 0 until metaChildNodes.length) { - val metaChildNode = metaChildNodes.item(metaChildPosition) - // - if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) { - val versionChildNodes = metaChildNode.childNodes - for (versionChildPosition in 0 until versionChildNodes.length) { - val versionChildNode = versionChildNodes.item(versionChildPosition) - if (versionChildNode.nodeType == Node.TEXT_NODE) { - val versionText = versionChildNode.textContent.removeSpaceChars() - try { - xmlKeyFileVersion = versionText.toFloat() - Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion") - } catch (e: Exception) { - Log.e(TAG, "XML Keyfile version cannot be read : $versionText") - } - } - } - } - } - } - // - if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) { - val keyChildNodes = keyFileChildNode.childNodes - for (keyChildPosition in 0 until keyChildNodes.length) { - val keyChildNode = keyChildNodes.item(keyChildPosition) - // - if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) { - var hashString : String? = null - if (keyChildNode.hasAttributes()) { - val dataNodeAttributes = keyChildNode.attributes - hashString = dataNodeAttributes - .getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue - } - val dataChildNodes = keyChildNode.childNodes - for (dataChildPosition in 0 until dataChildNodes.length) { - val dataChildNode = dataChildNodes.item(dataChildPosition) - if (dataChildNode.nodeType == Node.TEXT_NODE) { - val dataString = dataChildNode.textContent.removeSpaceChars() - when (xmlKeyFileVersion) { - 1F -> { - // No hash in KeyFile XML version 1 - return Base64.decode(dataString, BASE_64_FLAG) - } - 2F -> { - return if (hashString != null - && checkKeyFileHash(dataString, hashString)) { - Log.i(TAG, "Successful key file hash check.") - Hex.decodeHex(dataString.toCharArray()) - } else { - Log.e(TAG, "Unable to check the hash of the key file.") - null - } - } - } - } - } - } - } - } - } - } catch (e: Exception) { - return null - } - return null - } - - private fun checkKeyFileHash(data: String, hash: String): Boolean { - var success = false - try { - // hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key. - val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray())) - .copyOfRange(0, 4).toHexString() - success = dataDigest == hash - } catch (e: Exception) { - e.printStackTrace() - } - return success - } - override fun newGroupId(): NodeIdUUID { var newId: NodeIdUUID do { @@ -928,13 +871,6 @@ class DatabaseKDBX : DatabaseVersioned { private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited - private const val XML_NODE_ROOT_NAME = "KeyFile" - private const val XML_NODE_META_NAME = "Meta" - private const val XML_NODE_VERSION_NAME = "Version" - private const val XML_NODE_KEY_NAME = "Key" - private const val XML_NODE_DATA_NAME = "Data" - private const val XML_ATTRIBUTE_DATA_HASH = "Hash" - const val BASE_64_FLAG = Base64.NO_WRAP } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt index 3ffae91b2..31597608f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.database.element.database import android.util.Log -import com.kunzisoft.encrypt.HashManager import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.element.binary.AttachmentPool @@ -32,11 +31,9 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException -import org.apache.commons.codec.binary.Hex -import java.io.ByteArrayInputStream -import java.io.IOException import java.io.InputStream import java.io.UnsupportedEncodingException +import java.nio.charset.Charset import java.util.* abstract class DatabaseVersioned< @@ -46,7 +43,6 @@ abstract class DatabaseVersioned< Entry : EntryVersioned > { - // Algorithm used to encrypt the database abstract var encryptionAlgorithm: EncryptionAlgorithm abstract val availableEncryptionAlgorithms: List @@ -55,11 +51,12 @@ abstract class DatabaseVersioned< abstract val kdfAvailableList: List abstract var numberKeyEncryptionRounds: Long - protected abstract val passwordEncoding: String + abstract val passwordEncoding: Charset var masterKey = ByteArray(32) var finalKey: ByteArray? = null protected set + var transformSeed: ByteArray? = null abstract val version: String abstract val defaultFileExtension: String @@ -91,58 +88,6 @@ abstract class DatabaseVersioned< return getGroupIndexes().filter { it != rootGroup } } - @Throws(IOException::class) - protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray - - @Throws(IOException::class) - fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) { - masterKey = getMasterKey(key, keyfileInputStream) - } - - @Throws(IOException::class) - protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray { - val fileKey = getFileKey(keyfileInputStream) - val passwordKey = getPasswordKey(key) - return HashManager.hashSha256(passwordKey, fileKey) - } - - @Throws(IOException::class) - protected fun getPasswordKey(key: String): ByteArray { - val bKey: ByteArray = try { - key.toByteArray(charset(passwordEncoding)) - } catch (e: UnsupportedEncodingException) { - key.toByteArray() - } - return HashManager.hashSha256(bKey) - } - - @Throws(IOException::class) - protected fun getFileKey(keyInputStream: InputStream): ByteArray { - try { - val keyData = keyInputStream.readBytes() - - // Check XML key file - val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) - if (xmlKeyByteArray != null) { - return xmlKeyByteArray - } - - // Check 32 bytes key file - when (keyData.size) { - 32 -> return keyData - 64 -> try { - return Hex.decodeHex(String(keyData).toCharArray()) - } catch (ignoredException: Exception) { - // Key is not base 64, treat it as binary data - } - } - // Hash file as binary data - return HashManager.hashSha256(keyData) - } catch (outOfMemoryError: OutOfMemoryError) { - throw IOException("Keyfile data is too large", outOfMemoryError) - } - } - protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { return null } @@ -158,20 +103,25 @@ abstract class DatabaseVersioned< val bKey: ByteArray try { - bKey = password.toByteArray(charset(encoding)) + bKey = password.toByteArray(encoding) } catch (e: UnsupportedEncodingException) { return false } val reEncoded: String try { - reEncoded = String(bKey, charset(encoding)) + reEncoded = String(bKey, encoding) } catch (e: UnsupportedEncodingException) { return false } return password == reEncoded } + fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned) { + this.masterKey = databaseVersioned.masterKey + this.transformSeed = databaseVersioned.transformSeed + } + /* * ------------------------------------- * Node Creation diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt index 07f69ab4e..6cfd2eaaa 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt @@ -24,128 +24,172 @@ import androidx.annotation.StringRes import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import java.io.PrintStream +import java.io.PrintWriter abstract class DatabaseException : Exception { + var innerMessage: String? = null abstract var errorId: Int var parameters: (Array)? = null + var mThrowable: Throwable? = null constructor() : super() constructor(message: String) : super(message) - constructor(message: String, throwable: Throwable) : super(message, throwable) - constructor(throwable: Throwable) : super(throwable) + constructor(message: String, throwable: Throwable) { + mThrowable = throwable + innerMessage = StringBuilder().apply { + append(message) + if (throwable.localizedMessage != null) { + append(" ") + append(throwable.localizedMessage) + } + }.toString() + } + constructor(throwable: Throwable) { + mThrowable = throwable + innerMessage = throwable.localizedMessage + } fun getLocalizedMessage(resources: Resources): String { - parameters?.let { - return resources.getString(errorId, *it) - } ?: return resources.getString(errorId) + val throwable = mThrowable + if (throwable is DatabaseException) + errorId = throwable.errorId + val localMessage = parameters?.let { + resources.getString(errorId, *it) + } ?: resources.getString(errorId) + return StringBuilder().apply { + append(localMessage) + if (innerMessage != null) { + append(" ") + append(innerMessage) + } + }.toString() + } + + override fun printStackTrace() { + mThrowable?.printStackTrace() + super.printStackTrace() + } + + override fun printStackTrace(s: PrintStream) { + mThrowable?.printStackTrace(s) + super.printStackTrace(s) + } + + override fun printStackTrace(s: PrintWriter) { + mThrowable?.printStackTrace(s) + super.printStackTrace(s) } } -open class LoadDatabaseException : DatabaseException { +class FileNotFoundDatabaseException : DatabaseInputException() { @StringRes - override var errorId: Int = R.string.error_load_database - constructor() : super() - constructor(string: String) : super(string) - constructor(throwable: Throwable) : super(throwable) + override var errorId: Int = R.string.file_not_found_content } -class FileNotFoundDatabaseException : LoadDatabaseException { +class CorruptedDatabaseException : DatabaseInputException() { @StringRes - override var errorId: Int = R.string.file_not_found_content - constructor() : super() - constructor(string: String) : super(string) - constructor(exception: Throwable) : super(exception) + override var errorId: Int = R.string.corrupted_file } -class InvalidAlgorithmDatabaseException : LoadDatabaseException { +class InvalidAlgorithmDatabaseException : DatabaseInputException { @StringRes override var errorId: Int = R.string.invalid_algorithm constructor() : super() constructor(exception: Throwable) : super(exception) } -class DuplicateUuidDatabaseException: LoadDatabaseException { +class UnknownDatabaseLocationException : DatabaseException() { @StringRes - override var errorId: Int = R.string.invalid_db_same_uuid - constructor(type: Type, uuid: NodeId<*>) : super() { - parameters = arrayOf(type.name, uuid.toString()) - } - constructor(exception: Throwable) : super(exception) + override var errorId: Int = R.string.error_location_unknown } -class IODatabaseException : LoadDatabaseException { +class HardwareKeyDatabaseException : DatabaseException() { @StringRes - override var errorId: Int = R.string.error_load_database - constructor() : super() - constructor(string: String) : super(string) - constructor(exception: Throwable) : super(exception) + override var errorId: Int = R.string.error_hardware_key_unsupported } -class KDFMemoryDatabaseException : LoadDatabaseException { +class EmptyKeyDatabaseException : DatabaseException() { @StringRes - override var errorId: Int = R.string.error_load_database_KDF_memory - constructor() : super() - constructor(exception: Throwable) : super(exception) + override var errorId: Int = R.string.error_empty_key } -class SignatureDatabaseException : LoadDatabaseException { +class SignatureDatabaseException : DatabaseInputException() { @StringRes override var errorId: Int = R.string.invalid_db_sig - constructor() : super() - constructor(exception: Throwable) : super(exception) } -class VersionDatabaseException : LoadDatabaseException { +class VersionDatabaseException : DatabaseInputException() { @StringRes override var errorId: Int = R.string.unsupported_db_version - constructor() : super() - constructor(exception: Throwable) : super(exception) } -class InvalidCredentialsDatabaseException : LoadDatabaseException { +class InvalidCredentialsDatabaseException : DatabaseInputException { @StringRes override var errorId: Int = R.string.invalid_credentials constructor() : super() - constructor(exception: Throwable) : super(exception) + constructor(string: String) : super(string) } -class NoMemoryDatabaseException: LoadDatabaseException { +class KDFMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) { + @StringRes + override var errorId: Int = R.string.error_load_database_KDF_memory +} + +class NoMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) { @StringRes override var errorId: Int = R.string.error_out_of_memory +} + +class DuplicateUuidDatabaseException(type: Type, uuid: NodeId<*>) : DatabaseInputException() { + @StringRes + override var errorId: Int = R.string.invalid_db_same_uuid + init { + parameters = arrayOf(type.name, uuid.toString()) + } +} + +class XMLMalformedDatabaseException : DatabaseInputException { + @StringRes + override var errorId: Int = R.string.error_XML_malformed constructor() : super() - constructor(exception: Throwable) : super(exception) + constructor(string: String) : super(string) } -class MoveEntryDatabaseException: LoadDatabaseException { +class MergeDatabaseKDBException : DatabaseInputException() { + @StringRes + override var errorId: Int = R.string.error_unable_merge_database_kdb +} + +class MoveEntryDatabaseException : DatabaseException() { @StringRes override var errorId: Int = R.string.error_move_entry_here - constructor() : super() - constructor(exception: Throwable) : super(exception) } -class MoveGroupDatabaseException: LoadDatabaseException { +class MoveGroupDatabaseException : DatabaseException() { @StringRes override var errorId: Int = R.string.error_move_group_here - constructor() : super() - constructor(exception: Throwable) : super(exception) } -class CopyEntryDatabaseException: LoadDatabaseException { +class CopyEntryDatabaseException : DatabaseException() { @StringRes override var errorId: Int = R.string.error_copy_entry_here - constructor() : super() - constructor(exception: Throwable) : super(exception) } -class CopyGroupDatabaseException: LoadDatabaseException { +class CopyGroupDatabaseException : DatabaseException() { @StringRes override var errorId: Int = R.string.error_copy_group_here +} + +open class DatabaseInputException : DatabaseException { + @StringRes + override var errorId: Int = R.string.error_load_database constructor() : super() - constructor(exception: Throwable) : super(exception) + constructor(string: String) : super(string) + constructor(throwable: Throwable) : super(throwable) } -// TODO Output Exception open class DatabaseOutputException : DatabaseException { @StringRes override var errorId: Int = R.string.error_save_database diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt index 321876af4..f8d4f551d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt @@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.file.input import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.database.DatabaseVersioned -import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.DatabaseInputException import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import java.io.InputStream @@ -33,15 +33,9 @@ abstract class DatabaseInput> (protected var m /** * Load a versioned database file, return contents in a new DatabaseVersioned. - * - * @param databaseInputStream Existing file to load. - * @param password Pass phrase for infile. - * @return new DatabaseVersioned container. - * - * @throws LoadDatabaseException on database error (contains IO exceptions) */ - @Throws(LoadDatabaseException::class) + @Throws(DatabaseInputException::class) abstract fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, assignMasterKey: (() -> Unit)): D diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt index 8d9373ecd..c4ff17da9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt @@ -47,7 +47,7 @@ import javax.crypto.CipherInputStream class DatabaseInputKDB(database: DatabaseKDB) : DatabaseInput(database) { - @Throws(LoadDatabaseException::class) + @Throws(DatabaseInputException::class) override fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, assignMasterKey: (() -> Unit)): DatabaseKDB { @@ -76,6 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB) throw VersionDatabaseException() } + mDatabase.transformSeed = header.transformSeed assignMasterKey.invoke() // Select algorithm @@ -310,18 +311,11 @@ class DatabaseInputKDB(database: DatabaseKDB) stopContentTimer() - } catch (e: LoadDatabaseException) { + } catch (e: Error) { mDatabase.clearAll() - throw e - } catch (e: IOException) { - mDatabase.clearAll() - throw IODatabaseException(e) - } catch (e: OutOfMemoryError) { - mDatabase.clearAll() - throw NoMemoryDatabaseException(e) - } catch (e: Exception) { - mDatabase.clearAll() - throw LoadDatabaseException(e) + if (e is OutOfMemoryError) + throw NoMemoryDatabaseException(e) + throw DatabaseInputException(e) } return mDatabase diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt index ed6e748c7..ec0ca0e50 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt @@ -99,7 +99,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) this.isRAMSufficient = method } - @Throws(LoadDatabaseException::class) + @Throws(DatabaseInputException::class) override fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, assignMasterKey: (() -> Unit)): DatabaseKDBX { @@ -114,6 +114,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX) hashOfHeader = headerAndHash.hash val pbHeader = headerAndHash.header + val transformSeed = header.transformSeed + mDatabase.transformSeed = transformSeed assignMasterKey.invoke() mDatabase.makeFinalKey(header.masterSeed) @@ -155,7 +157,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) throw InvalidCredentialsDatabaseException() } - val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException() + val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException() val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES) val hmac: Mac = HmacBlock.getHmacSha256(blockKey) @@ -187,7 +189,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) try { randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey) } catch (e: Exception) { - throw LoadDatabaseException(e) + throw DatabaseInputException(e) } val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply { @@ -200,19 +202,12 @@ class DatabaseInputKDBX(database: DatabaseKDBX) stopContentTimer() - } catch (e: LoadDatabaseException) { - throw e - } catch (e: XmlPullParserException) { - throw IODatabaseException(e) - } catch (e: IOException) { + } catch (e: Error) { + if (e is OutOfMemoryError) + throw NoMemoryDatabaseException(e) if (e.message?.contains("Hash failed with code") == true) throw KDFMemoryDatabaseException(e) - else - throw IODatabaseException(e) - } catch (e: OutOfMemoryError) { - throw NoMemoryDatabaseException(e) - } catch (e: Exception) { - throw LoadDatabaseException(e) + throw DatabaseInputException(e) } return mDatabase @@ -227,7 +222,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) val fieldId = dataInputStream.read().toByte() val size = dataInputStream.readBytes4ToUInt().toKotlinInt() - if (size < 0) throw IOException("Corrupted file") + if (size < 0) throw CorruptedDatabaseException() var data = ByteArray(0) try { @@ -238,7 +233,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) } } catch (e: Exception) { // OOM only if corrupted file - throw IOException("Corrupted file") + throw CorruptedDatabaseException() } readStream = true @@ -297,7 +292,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) Binaries } - @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) + @Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class) private fun readDocumentStreamed(xpp: XmlPullParser) { ctxGroups.clear() @@ -324,11 +319,11 @@ class DatabaseInputKDBX(database: DatabaseKDBX) } // Error checks - if (ctx != KdbContext.Null) throw IOException("Malformed") - if (ctxGroups.size != 0) throw IOException("Malformed") + if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException() + if (ctxGroups.size != 0) throw XMLMalformedDatabaseException() } - @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) + @Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class) private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext { val name = xpp.name when (ctx) { @@ -352,7 +347,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) if (encodedHash.isNotEmpty() && hashOfHeader != null) { val hash = Base64.decode(encodedHash, BASE_64_FLAG) if (!Arrays.equals(hash, hashOfHeader)) { - throw LoadDatabaseException() + throw DatabaseInputException() } } } else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) { @@ -824,7 +819,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) if (ctx != null) { contextName = ctx.name } - throw RuntimeException("Invalid end element: Context " + contextName + "End element: " + name) + throw XMLMalformedDatabaseException("Invalid end element: Context " + contextName + "End element: " + name) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt index 4d456e538..e2d7a70e6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt @@ -26,7 +26,7 @@ import java.io.OutputStream import java.security.NoSuchAlgorithmException import java.security.SecureRandom -abstract class DatabaseOutput
protected constructor(protected var mOutputStream: OutputStream) { +abstract class DatabaseOutput
{ @Throws(DatabaseOutputException::class) protected open fun setIVs(header: Header): SecureRandom { @@ -44,9 +44,7 @@ abstract class DatabaseOutput
protected constructor(pro } @Throws(DatabaseOutputException::class) - abstract fun output() - - @Throws(DatabaseOutputException::class) - abstract fun outputHeader(outputStream: OutputStream): Header + abstract fun writeDatabase(outputStream: OutputStream, + assignMasterKey: () -> Unit) } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt index 96cfe0c1c..83333ec2d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt @@ -39,9 +39,8 @@ import java.security.* import javax.crypto.Cipher import javax.crypto.CipherOutputStream -class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, - outputStream: OutputStream) - : DatabaseOutput(outputStream) { +class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB) + : DatabaseOutput() { private var headerHashBlock: ByteArray? = null @@ -60,15 +59,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, } @Throws(DatabaseOutputException::class) - override fun output() { + override fun writeDatabase(outputStream: OutputStream, + assignMasterKey: () -> Unit) { // 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 // also remove the virtual root not present in kdb val rootGroup = mDatabaseKDB.rootGroup sortNodesForOutput() - val header = outputHeader(mOutputStream) - + val header = outputHeader(outputStream, assignMasterKey) val finalKey = getFinalKey(header) val cipher: Cipher = try { @@ -81,7 +80,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, } try { - val cos = CipherOutputStream(mOutputStream, cipher) + val cos = CipherOutputStream(outputStream, cipher) val bos = BufferedOutputStream(cos) outputPlanGroupAndEntries(bos) bos.flush() @@ -107,7 +106,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, } @Throws(DatabaseOutputException::class) - override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB { + private fun outputHeader(outputStream: OutputStream, + assignMasterKey: () -> Unit): DatabaseHeaderKDB { // Build header val header = DatabaseHeaderKDB() header.signature1 = DatabaseHeaderKDB.DBSIG_1 @@ -132,6 +132,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, setIVs(header) + mDatabaseKDB.transformSeed = header.transformSeed + assignMasterKey() + // Header checksum val headerDigest: MessageDigest = HashManager.getHash256() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt index 241fc10df..66e6afa16 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt @@ -56,9 +56,8 @@ import javax.crypto.CipherOutputStream import kotlin.experimental.or -class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, - outputStream: OutputStream) - : DatabaseOutput(outputStream) { +class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX) + : DatabaseOutput() { private var randomStream: StreamCipher? = null private lateinit var xml: XmlSerializer @@ -67,43 +66,34 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, private var headerHmac: ByteArray? = null @Throws(DatabaseOutputException::class) - override fun output() { + override fun writeDatabase(outputStream: OutputStream, + assignMasterKey: () -> Unit) { try { - header = outputHeader(mOutputStream) + header = outputHeader(outputStream, assignMasterKey) val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) { - val cos = attachStreamEncryptor(header!!, mOutputStream) + val cos = attachStreamEncryptor(header!!, outputStream) cos.write(header!!.streamStartBytes) HashedBlockOutputStream(cos) } else { - mOutputStream.write(hashOfHeader!!) - mOutputStream.write(headerHmac!!) + outputStream.write(hashOfHeader!!) + outputStream.write(headerHmac!!) - attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!)) + attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!)) } - val xmlOutputStream: OutputStream - try { - xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) { - CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain) - else -> osPlain - } - + when(mDatabaseKDBX.compressionAlgorithm) { + CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain) + else -> osPlain + }.use { xmlOutputStream -> if (!header!!.version.isBefore(FILE_VERSION_40)) { outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream) } - outputDatabase(xmlOutputStream) - xmlOutputStream.close() - } catch (e: IllegalArgumentException) { - throw DatabaseOutputException(e) - } catch (e: IllegalStateException) { - throw DatabaseOutputException(e) } - - } catch (e: IOException) { + } catch (e: Exception) { throw DatabaseOutputException(e) } } @@ -322,11 +312,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } @Throws(DatabaseOutputException::class) - override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX { + private fun outputHeader(outputStream: OutputStream, + assignMasterKey: () -> Unit): DatabaseHeaderKDBX { try { val header = DatabaseHeaderKDBX(mDatabaseKDBX) setIVs(header) + mDatabaseKDBX.transformSeed = header.transformSeed + assignMasterKey.invoke() + val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream) pho.output() diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt new file mode 100644 index 000000000..e4249a7e7 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt @@ -0,0 +1,35 @@ +package com.kunzisoft.keepass.hardware + +enum class HardwareKey(val value: String) { + // FIDO2_SECRET("FIDO2 secret"), + CHALLENGE_RESPONSE_YUBIKEY("Yubikey challenge-response"); + + override fun toString(): String { + return value + } + + companion object { + val DEFAULT = CHALLENGE_RESPONSE_YUBIKEY + + fun getStringValues(): List { + return values().map { it.value } + } + + fun fromPosition(position: Int): HardwareKey { + return when (position) { + // 0 -> FIDO2_SECRET + 0 -> CHALLENGE_RESPONSE_YUBIKEY + else -> DEFAULT + } + } + + fun getHardwareKeyFromString(text: String?): HardwareKey? { + if (text == null) + return null + values().find { it.value == text }?.let { + return it + } + return DEFAULT + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyActivity.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyActivity.kt new file mode 100644 index 000000000..e337739ae --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyActivity.kt @@ -0,0 +1,172 @@ +package com.kunzisoft.keepass.hardware + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import com.kunzisoft.keepass.BuildConfig +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.utils.UriUtil + +/** + * Special activity to deal with hardware key drivers, + * return the response to the database service once finished + */ +class HardwareKeyActivity: DatabaseModeActivity(){ + + // To manage hardware key challenge response + private val resultCallback = ActivityResultCallback { result -> + if (result.resultCode == Activity.RESULT_OK) { + val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY) + Log.d(TAG, "Response form challenge") + mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0)) + } else { + Log.e(TAG, "Response from challenge error") + mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0)) + } + finish() + } + + private var activityResultLauncher: ActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + resultCallback + ) + + override fun applyCustomStyle(): Boolean { + return false + } + + override fun showDatabaseDialog(): Boolean { + return false + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + + val hardwareKey = HardwareKey.getHardwareKeyFromString( + intent.getStringExtra(DATA_HARDWARE_KEY) + ) + if (isHardwareKeyAvailable(this, hardwareKey, true) { + mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0)) + }) { + when (hardwareKey) { + /* + HardwareKey.FIDO2_SECRET -> { + // TODO FIDO2 under development + throw Exception("FIDO2 not implemented") + } + */ + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { + launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED)) + } + else -> { + finish() + } + } + } + } + + private fun launchYubikeyChallengeForResponse(seed: ByteArray?) { + // Transform the seed before sending + var challenge: ByteArray? = null + if (seed != null) { + challenge = ByteArray(64) + seed.copyInto(challenge, 0, 0, 32) + challenge.fill(32, 32, 64) + } + // Send to the driver + activityResultLauncher.launch( + Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply { + putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge) + } + ) + Log.d(TAG, "Challenge sent") + } + + companion object { + private val TAG = HardwareKeyActivity::class.java.simpleName + + private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY" + private const val DATA_SEED = "DATA_SEED" + private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE" + private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge" + private const val HARDWARE_KEY_RESPONSE_KEY = "response" + + fun launchHardwareKeyActivity( + context: Context, + hardwareKey: HardwareKey, + seed: ByteArray? + ) { + context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply { + flags = FLAG_ACTIVITY_NEW_TASK + putExtra(DATA_HARDWARE_KEY, hardwareKey.value) + putExtra(DATA_SEED, seed) + }) + } + + fun isHardwareKeyAvailable( + context: Context, + hardwareKey: HardwareKey?, + showDialog: Boolean = true, + onDialogDismissed: DialogInterface.OnDismissListener? = null + ): Boolean { + if (hardwareKey == null) + return false + return when (hardwareKey) { + /* + HardwareKey.FIDO2_SECRET -> { + // TODO FIDO2 under development + if (showDialog) + UnderDevelopmentFeatureDialogFragment() + .show(activity.supportFragmentManager, "underDevFeatureDialog") + false + } + */ + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { + // Check available intent + val yubikeyDriverAvailable = + Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT) + .resolveActivity(context.packageManager) != null + if (showDialog && !yubikeyDriverAvailable + && context is Activity) + showHardwareKeyDriverNeeded(context, hardwareKey) { + onDialogDismissed?.onDismiss(it) + context.finish() + } + yubikeyDriverAvailable + } + } + } + + private fun showHardwareKeyDriverNeeded( + context: Context, + hardwareKey: HardwareKey, + onDialogDismissed: DialogInterface.OnDismissListener + ) { + val builder = AlertDialog.Builder(context) + builder + .setMessage( + context.getString(R.string.error_driver_required, hardwareKey.toString()) + ) + .setPositiveButton(R.string.download) { _, _ -> + UriUtil.openExternalApp( + context, + context.getString(R.string.key_driver_app_id), + context.getString(R.string.key_driver_url) + ) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setOnDismissListener(onDialogDismissed) + builder.create().show() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt b/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt index f3ed3dade..321facd97 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/CipherEncryptDatabase.kt @@ -41,11 +41,6 @@ class CipherEncryptDatabase(): Parcelable { parcel.readByteArray(specParameters) } - fun replaceContent(copy: CipherEncryptDatabase) { - this.encryptedValue = copy.encryptedValue - this.specParameters = copy.specParameters - } - override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeParcelable(databaseUri, flags) parcel.writeEnum(credentialStorage) diff --git a/app/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt b/app/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt index 578fc4075..92470c2fe 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/DatabaseFile.kt @@ -1,9 +1,11 @@ package com.kunzisoft.keepass.model import android.net.Uri +import com.kunzisoft.keepass.hardware.HardwareKey data class DatabaseFile(var databaseUri: Uri? = null, var keyFileUri: Uri? = null, + var hardwareKey: HardwareKey? = null, var databaseDecodedPath: String? = null, var databaseAlias: String? = null, var databaseFileExists: Boolean = false, diff --git a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt deleted file mode 100644 index e5ee12c72..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.kunzisoft.keepass.model - -import android.net.Uri -import android.os.Parcel -import android.os.Parcelable - -data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable { - - constructor(parcel: Parcel) : this( - parcel.readString(), - parcel.readParcelable(Uri::class.java.classLoader)) { - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(masterPassword) - parcel.writeParcelable(keyFileUri, flags) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): MainCredential { - return MainCredential(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/model/ProgressMessage.kt b/app/src/main/java/com/kunzisoft/keepass/model/ProgressMessage.kt new file mode 100644 index 000000000..4b24b0d10 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/ProgressMessage.kt @@ -0,0 +1,13 @@ +package com.kunzisoft.keepass.model + +import androidx.annotation.StringRes + +data class ProgressMessage( + @StringRes + var titleId: Int, + @StringRes + var messageId: Int? = null, + @StringRes + var warningId: Int? = null, + var cancelable: (() -> Unit)? = null +) diff --git a/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt index 6d6bbbbd9..fa6650ff8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt @@ -1,3 +1,22 @@ +/* + * Copyright 2021 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ package com.kunzisoft.keepass.model import android.content.Context diff --git a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index fb04544a7..d8a70e9e9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -27,6 +27,7 @@ import android.os.Build import android.os.Bundle import android.os.IBinder import android.util.Log +import androidx.annotation.StringRes import androidx.media.app.NotificationCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.GroupActivity @@ -37,12 +38,15 @@ import com.kunzisoft.keepass.database.action.node.* import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.hardware.HardwareKeyActivity import com.kunzisoft.keepass.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.model.ProgressMessage import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater @@ -53,6 +57,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.closeDatabase import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import java.util.* open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater { @@ -61,20 +66,24 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private var mDatabase: Database? = null + // File description + private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null + private var mLastLocalSaveTime: Long = 0 + private val mainScope = CoroutineScope(Dispatchers.Main) - private var mDatabaseListeners = LinkedList() - private var mDatabaseInfoListeners = LinkedList() + private var mDatabaseListeners = mutableListOf() + private var mDatabaseInfoListeners = mutableListOf() private var mActionTaskBinder = ActionTaskBinder() - private var mActionTaskListeners = LinkedList() + private var mActionTaskListeners = mutableListOf() + // Channel to connect asynchronously a response + private var mResponseChallengeChannel: Channel? = null + private var mActionRunning = false private var mTaskRemovedRequested = false - private var mCreationState = false + private var mSaveState = false - private var mIconId: Int = R.drawable.notification_ic_database_load - private var mTitleId: Int = R.string.database_opened - private var mMessageId: Int? = null - private var mWarningId: Int? = null + private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened) override fun retrieveChannelId(): String { return CHANNEL_DATABASE_ID @@ -126,9 +135,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } interface ActionTaskListener { - fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) - fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) - fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) + fun onStartAction(database: Database, + progressMessage: ProgressMessage) + fun onUpdateAction(database: Database, + progressMessage: ProgressMessage) + fun onStopAction(database: Database, + actionTask: String, + result: ActionRunnable.Result) + } + + interface RequestChallengeListener { + fun onChallengeResponseRequested( + hardwareKey: HardwareKey, + seed: ByteArray? + ) } fun checkDatabase() { @@ -165,7 +185,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress Log.i(TAG, "Database file modified " + "$previousDatabaseInfo != $lastFileDatabaseInfo ") // Call listener to indicate a change in database info - if (!mCreationState && previousDatabaseInfo != null) { + if (!mSaveState && previousDatabaseInfo != null) { mDatabaseInfoListeners.forEach { listener -> listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo) } @@ -197,12 +217,47 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress mDatabase?.let { database -> if (mActionRunning) { mActionTaskListeners.forEach { actionTaskListener -> - actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId) + actionTaskListener.onStartAction( + database, mProgressMessage + ) } } } } + @OptIn(ExperimentalCoroutinesApi::class) + private fun sendResponseToChallenge(response: ByteArray) { + mainScope.launch { + val responseChannel = mResponseChallengeChannel + if (responseChannel == null || responseChannel.isEmpty) { + if (response.isEmpty()) { + cancelChallengeResponse(R.string.error_no_response_from_challenge) + } else { + mResponseChallengeChannel?.send(response) + } + } else { + cancelChallengeResponse(R.string.error_response_already_provided) + } + } + } + + private fun initializeChallengeResponse() { + // Init the channels + if (mResponseChallengeChannel == null) { + mResponseChallengeChannel = Channel(0) + } + } + + private fun closeChallengeResponse() { + mResponseChallengeChannel?.close() + mResponseChallengeChannel = null + } + + private fun cancelChallengeResponse(@StringRes error: Int) { + mResponseChallengeChannel?.cancel(CancellationException(getString(error))) + mResponseChallengeChannel = null + } + override fun onBind(intent: Intent): IBinder? { super.onBind(intent) return mActionTaskBinder @@ -219,8 +274,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } + // Get save state + mSaveState = if (intent != null) { + if (intent.hasExtra(SAVE_DATABASE_KEY)) { + !database.isReadOnly && intent.getBooleanExtra( + SAVE_DATABASE_KEY, + mSaveState + ) + } else (intent.action == ACTION_DATABASE_CREATE_TASK + || intent.action == ACTION_DATABASE_ASSIGN_PASSWORD_TASK + || intent.action == ACTION_DATABASE_SAVE) + } else false + // Create the notification - buildMessage(intent, database.isReadOnly) + buildNotification(intent) val intentAction = intent?.action @@ -258,7 +325,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK, ACTION_DATABASE_UPDATE_PARALLELISM_TASK, ACTION_DATABASE_UPDATE_ITERATIONS_TASK -> buildDatabaseUpdateElementActionTask(intent, database) - ACTION_DATABASE_SAVE -> buildDatabaseSave(intent, database) + ACTION_DATABASE_SAVE -> buildDatabaseSaveActionTask(intent, database) + ACTION_CHALLENGE_RESPONDED -> buildChallengeRespondedActionTask(intent) else -> null } @@ -272,15 +340,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress mActionRunning = true sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply { - putExtra(DATABASE_TASK_TITLE_KEY, mTitleId) - putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId) - putExtra(DATABASE_TASK_WARNING_KEY, mWarningId) + putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId) + putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId) + putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId) }) mActionTaskListeners.forEach { actionTaskListener -> - actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId) + actionTaskListener.onStartAction( + database, mProgressMessage + ) } - }, { actionRunnable @@ -325,7 +394,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress try { startService(Intent(applicationContext, DatabaseTaskNotificationService::class.java)) - } catch (e: IllegalStateException) {} + } catch (e: IllegalStateException) { + Log.w(TAG, "Cannot restart the database task service", e) + } } } mTaskRemovedRequested = false @@ -353,61 +424,51 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } - private fun buildMessage(intent: Intent?, readOnly: Boolean) { + private fun buildNotification(intent: Intent?) { // Assign elements for updates val intentAction = intent?.action - var saveAction = false - if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) { - saveAction = !readOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction) - } - - mIconId = if (intentAction == null) + // Get icon depending action state + val iconId = if (intentAction == null) R.drawable.notification_ic_database_open else - R.drawable.notification_ic_database_load + R.drawable.notification_ic_database_action - mTitleId = when { - saveAction -> { - R.string.saving_database - } - intentAction == null -> { + // Title depending on action + mProgressMessage.titleId = + if (intentAction == null) { R.string.database_opened - } - else -> { - when (intentAction) { - ACTION_DATABASE_CREATE_TASK -> R.string.creating_database - ACTION_DATABASE_LOAD_TASK, - ACTION_DATABASE_MERGE_TASK, - ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database - ACTION_DATABASE_SAVE -> R.string.saving_database - else -> { + } else when (intentAction) { + ACTION_DATABASE_CREATE_TASK -> R.string.creating_database + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_MERGE_TASK, + ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database + ACTION_DATABASE_ASSIGN_PASSWORD_TASK, + ACTION_DATABASE_SAVE -> R.string.saving_database + else -> { + if (mSaveState) + R.string.saving_database + else R.string.command_execution - } } } - } - mMessageId = when (intentAction) { - ACTION_DATABASE_LOAD_TASK, - ACTION_DATABASE_MERGE_TASK, - ACTION_DATABASE_RELOAD_TASK -> null - else -> null - } + // Updated later + mProgressMessage.messageId = null - mWarningId = - if (!saveAction - || intentAction == ACTION_DATABASE_LOAD_TASK - || intentAction == ACTION_DATABASE_MERGE_TASK - || intentAction == ACTION_DATABASE_RELOAD_TASK) - null - else + // Warning if data is saved + mProgressMessage.warningId = + if (mSaveState) R.string.do_not_kill_app + else + null val notificationBuilder = buildNewNotification().apply { - setSmallIcon(mIconId) + setSmallIcon(iconId) intent?.let { - setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId))) + setContentTitle(getString( + intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)) + ) } setAutoCancel(false) setContentIntent(null) @@ -513,15 +574,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } - override fun updateMessage(resId: Int) { - mMessageId = resId + private fun notifyProgressMessage() { mDatabase?.let { database -> mActionTaskListeners.forEach { actionTaskListener -> - actionTaskListener.onUpdateAction(database, mTitleId, mMessageId, mWarningId) + actionTaskListener.onUpdateAction( + database, mProgressMessage + ) } } } + override fun updateMessage(resId: Int) { + mProgressMessage.messageId = resId + notifyProgressMessage() + } + override fun actionOnLock() { if (!TimeoutHelper.temporarilyDisableLock) { closeDatabase(mDatabase) @@ -539,6 +606,43 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress super.onTaskRemoved(rootIntent) } + private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey, + seed: ByteArray?): ByteArray { + // Request a challenge - response + var response: ByteArray + runBlocking { + // Initialize the channels + initializeChallengeResponse() + val previousMessage = mProgressMessage.copy() + mProgressMessage.apply { + messageId = R.string.waiting_challenge_request + cancelable = { + cancelChallengeResponse(R.string.error_cancel_by_user) + } + } + // Send the request + notifyProgressMessage() + HardwareKeyActivity + .launchHardwareKeyActivity( + this@DatabaseTaskNotificationService, + hardwareKey, + seed + ) + // Wait the response + mProgressMessage.apply { + messageId = R.string.waiting_challenge_response + } + notifyProgressMessage() + response = mResponseChallengeChannel?.receive() ?: byteArrayOf() + // Close channels + closeChallengeResponse() + // Restore previous message + mProgressMessage = previousMessage + notifyProgressMessage() + } + return response + } + private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) @@ -550,15 +654,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress if (databaseUri == null) return null - mCreationState = true - return CreateDatabaseRunnable(this, database, databaseUri, getString(R.string.database_default_name), getString(R.string.database), getString(R.string.template_group_name), - mainCredential + mainCredential, + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } ) { result -> result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) @@ -586,17 +691,18 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress if (databaseUri == null) return null - mCreationState = false - return LoadDatabaseRunnable( - this, - database, - databaseUri, - mainCredential, - readOnly, - cipherEncryptDatabase, - intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), - this + this, + database, + databaseUri, + mainCredential, + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }, + readOnly, + cipherEncryptDatabase, + intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), + this ) { result -> // Add each info to reload database after thrown duplicate UUID exception result.data = Bundle().apply { @@ -623,9 +729,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress return MergeDatabaseRunnable( this, - database, databaseToMergeUri, databaseToMergeMainCredential, + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }, + database, + !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }, this ) { result -> // No need to add each info to reload database @@ -653,7 +766,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress database, databaseUri, intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() - ) + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } else { null } @@ -687,7 +802,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress newGroup, parent, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -712,7 +830,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress oldGroup, newGroup, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -737,7 +858,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress newEntry, parent, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -762,7 +886,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress oldEntry, newEntry, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -783,7 +910,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress getListNodesFromBundle(database, intent.extras!!), newParent, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -804,7 +934,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress getListNodesFromBundle(database, intent.extras!!), newParent, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -820,7 +953,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress database, getListNodesFromBundle(database, intent.extras!!), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), - AfterActionNodesRunnable()) + AfterActionNodesRunnable() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } else { null } @@ -838,7 +974,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress database, mainEntry, intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1), - !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)) + !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -857,7 +996,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress database, mainEntry, intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1), - !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)) + !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } } else { null @@ -881,7 +1023,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress oldElement, newElement, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) - ).apply { + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }.apply { mAfterSaveDatabase = { result -> result.data = intent.extras } @@ -897,7 +1041,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress return RemoveUnlinkedDataDatabaseRunnable(this, database, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) - ).apply { + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }.apply { mAfterSaveDatabase = { result -> result.data = intent.extras } @@ -911,7 +1057,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress return if (intent.hasExtra(SAVE_DATABASE_KEY)) { return SaveDatabaseRunnable(this, database, - !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) + !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + null, + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } ).apply { mAfterSaveDatabase = { result -> result.data = intent.extras @@ -925,7 +1075,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress /** * Save database without parameter */ - private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? { + private fun buildDatabaseSaveActionTask(intent: Intent, database: Database): ActionRunnable? { return if (intent.hasExtra(SAVE_DATABASE_KEY)) { var databaseCopyUri: Uri? = null @@ -936,12 +1086,34 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress SaveDatabaseRunnable(this, database, !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), + null, + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }, databaseCopyUri) } else { null } } + private fun buildChallengeRespondedActionTask(intent: Intent): ActionRunnable? { + return if (intent.hasExtra(DATA_BYTES)) { + object : ActionRunnable() { + override fun onStartRun() {} + override fun onActionRun() { + mainScope.launch { + intent.getByteArrayExtra(DATA_BYTES)?.let { response -> + sendResponseToChallenge(response) + } + } + } + override fun onFinishRun() {} + } + } else { + null + } + } + companion object { private val TAG = DatabaseTaskNotificationService::class.java.name @@ -978,6 +1150,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK" const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK" const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE" + const val ACTION_CHALLENGE_RESPONDED = "ACTION_CHALLENGE_RESPONDED" const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY" const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY" @@ -1001,9 +1174,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val NEW_NODES_KEY = "NEW_NODES_KEY" const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time - - private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null - private var mLastLocalSaveTime: Long = 0 + const val DATA_BYTES = "DATA_BYTES" fun getListNodesFromBundle(database: Database, bundle: Bundle): List { val nodesAction = ArrayList() diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt index 3c2c7c0d8..1d0390ad7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt @@ -115,8 +115,8 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev mDatabaseViewModel.saveDatabase(save) } - private fun mergeDatabase() { - mDatabaseViewModel.mergeDatabase(false) + private fun mergeDatabase(save: Boolean) { + mDatabaseViewModel.mergeDatabase(save) } private fun reloadDatabase() { @@ -671,7 +671,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev true } R.id.menu_merge_database -> { - mergeDatabase() + mergeDatabase(!mDatabaseReadOnly) true } R.id.menu_reload_database -> { 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 77fcdc552..a0557c89e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -96,6 +96,12 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.remember_keyfile_locations_default)) } + fun rememberHardwareKey(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.remember_hardware_key_key), + context.resources.getBoolean(R.bool.remember_hardware_key_default)) + } + fun automaticallyFocusSearch(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.auto_focus_search_key), @@ -479,29 +485,33 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.enable_keep_screen_on_default)) } + fun isScreenshotModeEnabled(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.enable_screenshot_mode_key), + context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default)) + } + fun isAdvancedUnlockEnable(context: Context): Boolean { return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context) } fun isBiometricUnlockEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - AdvancedUnlockManager.biometricUnlockSupported(context) - } else { - false - } return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), context.resources.getBoolean(R.bool.biometric_unlock_enable_default)) - && biometricSupported + && (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + AdvancedUnlockManager.biometricUnlockSupported(context) + } else { + false + }) } fun isDeviceCredentialUnlockEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) // Priority to biometric unlock - val biometricAlreadySupported = isBiometricUnlockEnable(context) return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key), context.resources.getBoolean(R.bool.device_credential_unlock_enable_default)) - && !biometricAlreadySupported + && !isBiometricUnlockEnable(context) } fun isTempAdvancedUnlockEnable(context: Context): Boolean { 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 e748e92e8..3ddb6d60c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.view.showActionErrorIfNeeded diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt index 26bb26565..e9240b73e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/ProgressTaskDialogFragment.kt @@ -24,15 +24,18 @@ import android.app.Dialog import android.os.Bundle import android.util.Log import android.view.View +import android.widget.Button import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import com.kunzisoft.keepass.R -import java.lang.Exception +import kotlinx.coroutines.launch -open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { +open class ProgressTaskDialogFragment : DialogFragment() { @StringRes private var title = UNDEFINED @@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { private var message = UNDEFINED @StringRes private var warning = UNDEFINED + private var cancellable: (() -> Unit)? = null private var titleView: TextView? = null private var messageView: TextView? = null private var warningView: TextView? = null + private var cancelButton: Button? = null private var progressView: ProgressBar? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { titleView = root.findViewById(R.id.progress_dialog_title) messageView = root.findViewById(R.id.progress_dialog_message) warningView = root.findViewById(R.id.progress_dialog_warning) + cancelButton = root.findViewById(R.id.progress_dialog_cancel) progressView = root.findViewById(R.id.progress_dialog_bar) updateTitle(title) updateMessage(message) updateWarning(warning) + setCancellable(cancellable) isCancelable = false @@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { } private fun updateView(textView: TextView?, @StringRes resId: Int) { - activity?.runOnUiThread { + activity?.lifecycleScope?.launch { if (resId == UNDEFINED) { textView?.visibility = View.GONE } else { @@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { } } - fun updateTitle(@StringRes resId: Int) { - this.title = resId + private fun updateCancelable() { + activity?.lifecycleScope?.launch { + cancelButton?.isVisible = cancellable != null + cancelButton?.setOnClickListener { + cancellable?.invoke() + } + } + } + + fun updateTitle(@StringRes resId: Int?) { + this.title = resId ?: UNDEFINED updateView(titleView, title) } - override fun updateMessage(@StringRes resId: Int) { - this.message = resId + fun updateMessage(@StringRes resId: Int?) { + this.message = resId ?: UNDEFINED updateView(messageView, message) } - fun updateWarning(@StringRes resId: Int) { - this.warning = resId + fun updateWarning(@StringRes resId: Int?) { + this.warning = resId ?: UNDEFINED updateView(warningView, warning) } + fun setCancellable(cancellable: (() -> Unit)?) { + this.cancellable = cancellable + updateCancelable() + } + companion object { private val TAG = ProgressTaskDialogFragment::class.java.simpleName const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment" diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt index d578df6ca..a794fa0ca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/ParcelableUtil.kt @@ -126,6 +126,26 @@ object ParcelableUtil { } } +fun Parcel.readByteArrayCompat(): ByteArray? { + val dataLength = readInt() + return if (dataLength >= 0) { + val data = ByteArray(dataLength) + readByteArray(data) + data + } else { + null + } +} + +fun Parcel.writeByteArrayCompat(data: ByteArray?) { + if (data != null) { + writeInt(data.size) + writeByteArray(data) + } else { + writeInt(-1) + } +} + inline fun > Parcel.readEnum() = readString()?.let { enumValueOf(it) } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt index 1ca58508e..c6c8428b3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -28,6 +28,7 @@ import android.os.Build import android.util.Log import android.widget.Toast import androidx.documentfile.provider.DocumentFile +import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.education.Education @@ -226,10 +227,10 @@ object UriUtil { } } - fun getUriFromIntent(intent: Intent, key: String): Uri? { + fun getUriFromIntent(intent: Intent?, key: String): Uri? { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - val clipData = intent.clipData + val clipData = intent?.clipData if (clipData != null) { if (clipData.description.label == key) { if (clipData.itemCount == 1) { @@ -242,7 +243,7 @@ object UriUtil { } } } catch (e: Exception) { - return intent.getParcelableExtra(key) + return intent?.getParcelableExtra(key) } return null } @@ -269,11 +270,15 @@ object UriUtil { fun contributingUser(context: Context): Boolean { return (Education.isEducationScreenReclickedPerformed(context) - || isExternalAppInstalled(context, "com.kunzisoft.keepass.pro", false) + || isExternalAppInstalled( + context, + context.getString(R.string.keepro_app_id), + false + ) ) } - private fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean { + fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean { try { context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) Education.setEducationScreenReclickedPerformed(context) @@ -285,20 +290,35 @@ object UriUtil { return false } - fun openExternalApp(context: Context, packageName: String) { + fun openExternalApp(context: Context, packageName: String, sourcesURL: String? = null) { var launchIntent: Intent? = null try { launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - } catch (ignored: Exception) { - } + } catch (ignored: Exception) { } try { if (launchIntent == null) { context.startActivity( Intent(Intent.ACTION_VIEW) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName")) + .setData( + Uri.parse( + if (sourcesURL != null + && !BuildConfig.CLOSED_STORE + ) { + sourcesURL + } else { + context.getString( + if (BuildConfig.CLOSED_STORE) + R.string.play_store_url + else + R.string.f_droid_url, + packageName + ) + } + ) + ) ) } else { context.startActivity(launchIntent) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt new file mode 100644 index 000000000..f2d53d46c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt @@ -0,0 +1,149 @@ +package com.kunzisoft.keepass.view + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import android.text.InputType +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Filter +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.google.android.material.textfield.TextInputLayout +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum + + +class HardwareKeySelectionView @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0) + : ConstraintLayout(context, attrs, defStyle) { + + private var mHardwareKey: HardwareKey? = null + + private val hardwareKeyLayout: TextInputLayout + private val hardwareKeyCompletion: AppCompatAutoCompleteTextView + var selectionListener: ((HardwareKey)-> Unit)? = null + + private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context) + + private class ArrayAdapterNoFilter(context: Context) + : ArrayAdapter(context, android.R.layout.simple_list_item_1) { + val hardwareKeys = HardwareKey.values() + + override fun getCount(): Int { + return hardwareKeys.size + } + + override fun getItem(position: Int): String { + return hardwareKeys[position].value + } + + override fun getItemId(position: Int): Long { + // Or just return p0 + return hardwareKeys[position].hashCode().toLong() + } + + override fun getFilter(): Filter { + return object : Filter() { + override fun performFiltering(p0: CharSequence?): FilterResults { + return FilterResults().apply { + values = hardwareKeys + } + } + + override fun publishResults(p0: CharSequence?, p1: FilterResults?) { + notifyDataSetChanged() + } + } + } + } + + init { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? + inflater?.inflate(R.layout.view_hardware_key_selection, this) + + hardwareKeyLayout = findViewById(R.id.input_entry_hardware_key_layout) + hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion) + + hardwareKeyCompletion.isFocusable = false + hardwareKeyCompletion.isFocusableInTouchMode = false + //hardwareKeyCompletion.isEnabled = false + hardwareKeyCompletion.isCursorVisible = false + hardwareKeyCompletion.setTextIsSelectable(false) + hardwareKeyCompletion.inputType = InputType.TYPE_NULL + hardwareKeyCompletion.setAdapter(mHardwareKeyAdapter) + + hardwareKeyCompletion.setOnClickListener { + hardwareKeyCompletion.showDropDown() + } + hardwareKeyCompletion.onItemClickListener = + AdapterView.OnItemClickListener { _, _, position, _ -> + mHardwareKey = HardwareKey.fromPosition(position) + mHardwareKey?.let { hardwareKey -> + selectionListener?.invoke(hardwareKey) + } + } + } + + var hardwareKey: HardwareKey? + get() { + return mHardwareKey + } + set(value) { + mHardwareKey = value + hardwareKeyCompletion.setText(value?.toString() ?: "") + } + + var error: CharSequence? + get() = hardwareKeyLayout.error + set(value) { + hardwareKeyLayout.error = value + } + + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + val saveState = SavedState(superState) + saveState.mHardwareKey = this.mHardwareKey + return saveState + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is SavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + this.mHardwareKey = state.mHardwareKey + } + + internal class SavedState : BaseSavedState { + var mHardwareKey: HardwareKey? = null + + constructor(superState: Parcelable?) : super(superState) + + private constructor(parcel: Parcel) : super(parcel) { + mHardwareKey = parcel.readEnum() + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeEnum(mHardwareKey) + } + + companion object CREATOR : Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt index 75ba32eb4..325e18ba1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -39,19 +39,24 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.model.CredentialStorage -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential +import com.kunzisoft.keepass.hardware.HardwareKey class MainCredentialView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : FrameLayout(context, attrs, defStyle) { - private var passwordTextView: EditText - private var keyFileSelectionView: KeyFileSelectionView private var checkboxPasswordView: CompoundButton + private var passwordTextView: EditText private var checkboxKeyFileView: CompoundButton + private var keyFileSelectionView: KeyFileSelectionView + private var checkboxHardwareView: CompoundButton + private var hardwareKeySelectionView: HardwareKeySelectionView var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null + var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null + var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null var onValidateListener: (() -> Unit)? = null private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD @@ -60,15 +65,17 @@ class MainCredentialView @JvmOverloads constructor(context: Context, val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? inflater?.inflate(R.layout.view_main_credentials, this) + checkboxPasswordView = findViewById(R.id.password_checkbox) passwordTextView = findViewById(R.id.password_text_view) + checkboxKeyFileView = findViewById(R.id.keyfile_checkbox) keyFileSelectionView = findViewById(R.id.keyfile_selection) - checkboxPasswordView = findViewById(R.id.password_checkbox) - checkboxKeyFileView = findViewById(R.id.keyfile_checkox) + checkboxHardwareView = findViewById(R.id.hardware_key_checkbox) + hardwareKeySelectionView = findViewById(R.id.hardware_key_selection) val onEditorActionListener = object : TextView.OnEditorActionListener { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { if (actionId == EditorInfo.IME_ACTION_DONE) { - onValidateListener?.invoke() + validateCredential() return true } return false @@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, if (keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER ) { - onValidateListener?.invoke() + validateCredential() handled = true } handled @@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context, checkboxPasswordView.setOnCheckedChangeListener { view, checked -> onPasswordChecked?.onCheckedChanged(view, checked) } + checkboxKeyFileView.setOnCheckedChangeListener { view, checked -> + if (checked) { + if (keyFileSelectionView.uri == null) { + checkboxKeyFileView.isChecked = false + } + } + onKeyFileChecked?.onCheckedChanged(view, checked) + } + checkboxHardwareView.setOnCheckedChangeListener { view, checked -> + if (checked) { + if (hardwareKeySelectionView.hardwareKey == null) { + checkboxHardwareView.isChecked = false + } + } + onHardwareKeyChecked?.onCheckedChanged(view, checked) + } + + hardwareKeySelectionView.selectionListener = { _ -> + checkboxHardwareView.isChecked = true + } } - fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { - keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) + fun validateCredential() { + onValidateListener?.invoke() } fun populatePasswordTextView(text: String?) { @@ -118,7 +145,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } - fun populateKeyFileTextView(uri: Uri?) { + fun populateKeyFileView(uri: Uri?) { if (uri == null || uri.toString().isEmpty()) { keyFileSelectionView.uri = null if (checkboxKeyFileView.isChecked) @@ -130,16 +157,36 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } + fun populateHardwareKeyView(hardwareKey: HardwareKey?) { + if (hardwareKey == null) { + hardwareKeySelectionView.hardwareKey = null + if (checkboxHardwareView.isChecked) + checkboxHardwareView.isChecked = false + } else { + hardwareKeySelectionView.hardwareKey = hardwareKey + if (!checkboxHardwareView.isChecked) + checkboxHardwareView.isChecked = true + } + } + + fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { + keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) + } + fun isFill(): Boolean { - return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked + return checkboxPasswordView.isChecked + || (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null) + || (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null) } fun getMainCredential(): MainCredential { return MainCredential().apply { - this.masterPassword = if (checkboxPasswordView.isChecked) + this.password = if (checkboxPasswordView.isChecked) passwordTextView.text?.toString() else null this.keyFileUri = if (checkboxKeyFileView.isChecked) keyFileSelectionView.uri else null + this.hardwareKey = if (checkboxHardwareView.isChecked) + hardwareKeySelectionView.hardwareKey else null } } @@ -151,7 +198,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, // TODO HARDWARE_KEY return when (mCredentialStorage) { CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked - CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked + CredentialStorage.KEY_FILE -> false CredentialStorage.HARDWARE_KEY -> false } } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt index 2c7b4e968..a0f30b977 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt @@ -68,7 +68,7 @@ class TemplateView @JvmOverloads constructor(context: Context, setCopyButtonState(TextFieldView.ButtonState.ACTIVATE) setCopyButtonClickListener { label, value -> mOnCopyActionClickListener - ?.invoke(Field(label, ProtectedString(false, value))) + ?.invoke(Field(label, ProtectedString(true, value))) } } else { setCopyButtonState(TextFieldView.ButtonState.GONE) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 44182c9ee..f9de51654 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -226,8 +226,8 @@ fun View.updateLockPaddingLeft() { fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) { if (!result.isSuccess) { - result.exception?.errorId?.let { errorId -> - Toast.makeText(this, errorId, Toast.LENGTH_LONG).show() + result.exception?.getLocalizedMessage(resources)?.let { errorMessage -> + Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show() } ?: result.message?.let { message -> Toast.makeText(this, message, Toast.LENGTH_LONG).show() } @@ -236,8 +236,8 @@ fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) { fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) { if (!result.isSuccess) { - result.exception?.errorId?.let { errorId -> - Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show() + result.exception?.getLocalizedMessage(resources)?.let { errorMessage -> + Snackbar.make(this, errorMessage, Snackbar.LENGTH_LONG).asError().show() } ?: result.message?.let { message -> Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show() } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt index f1383f55d..0fafefbc6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.IOActionTask +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.UriUtil @@ -72,8 +73,12 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic } } - fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) { - mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded -> + fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?, hardwareKey: HardwareKey?) { + mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri( + databaseUri, + keyFileUri, + hardwareKey + ) { databaseFileAdded -> databaseFileAdded?.let { _ -> databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply { this.databaseFileAction = DatabaseFileAction.ADD @@ -96,6 +101,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic .find { it.databaseUri == databaseFileUpdated.databaseUri } ?.apply { keyFileUri = databaseFileUpdated.keyFileUri + hardwareKey = databaseFileUpdated.hardwareKey databaseAlias = databaseFileUpdated.databaseAlias databaseFileExists = databaseFileUpdated.databaseFileExists databaseLastModified = databaseFileUpdated.databaseLastModified diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt index 8608b144a..99a89642d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt @@ -87,8 +87,8 @@ class DatabaseViewModel: ViewModel() { _saveDatabase.value = save } - fun mergeDatabase(fixDuplicateUuid: Boolean) { - _mergeDatabase.value = fixDuplicateUuid + fun mergeDatabase(save: Boolean) { + _mergeDatabase.value = save } fun reloadDatabase(fixDuplicateUuid: Boolean) { @@ -196,6 +196,8 @@ class DatabaseViewModel: ViewModel() { data class SuperLong(val oldValue: Long, val newValue: Long, val save: Boolean) + data class SuperMerge(val fixDuplicateUuid: Boolean, + val save: Boolean) data class SuperCompression(val oldValue: CompressionAlgorithm, val newValue: CompressionAlgorithm, val save: Boolean) diff --git a/app/src/main/res/drawable-hdpi/notification_ic_database_load.png b/app/src/main/res/drawable-hdpi/notification_ic_database_action.png similarity index 100% rename from app/src/main/res/drawable-hdpi/notification_ic_database_load.png rename to app/src/main/res/drawable-hdpi/notification_ic_database_action.png diff --git a/app/src/main/res/drawable-mdpi/notification_ic_database_load.png b/app/src/main/res/drawable-mdpi/notification_ic_database_action.png similarity index 100% rename from app/src/main/res/drawable-mdpi/notification_ic_database_load.png rename to app/src/main/res/drawable-mdpi/notification_ic_database_action.png diff --git a/app/src/main/res/drawable-xhdpi/notification_ic_database_load.png b/app/src/main/res/drawable-xhdpi/notification_ic_database_action.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/notification_ic_database_load.png rename to app/src/main/res/drawable-xhdpi/notification_ic_database_action.png diff --git a/app/src/main/res/drawable-xxhdpi/notification_ic_database_load.png b/app/src/main/res/drawable-xxhdpi/notification_ic_database_action.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/notification_ic_database_load.png rename to app/src/main/res/drawable-xxhdpi/notification_ic_database_action.png diff --git a/app/src/main/res/drawable-xxxhdpi/notification_ic_database_load.png b/app/src/main/res/drawable-xxxhdpi/notification_ic_database_action.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/notification_ic_database_load.png rename to app/src/main/res/drawable-xxxhdpi/notification_ic_database_action.png diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 68390273c..d6eab3c13 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -34,7 +34,7 @@ android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintTop_toBottomOf="@id/toolbar" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"> + - \ No newline at end of file + + diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index 96c2b2937..f7e355eeb 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -17,165 +17,174 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - - + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" + app:layout_constraintTop_toTopOf="parent"> - - - + + - + + + + + + + + + + + + + + + + + + + - - - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:paddingTop="12dp" + android:paddingStart="5dp" + android:paddingLeft="5dp" + android:paddingEnd="5dp" + android:paddingRight="5dp" + android:layout_gravity="center" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:orientation="horizontal" + app:layout_constraintTop_toBottomOf="@+id/history_container"/> + + + - - + - + + + - + + + + - - - - - - - - - - - - - + android:indeterminate="true" /> + - - - - - - - + android:layout_gravity="start|bottom" /> - + - \ No newline at end of file + + diff --git a/app/src/main/res/layout/activity_entry_edit.xml b/app/src/main/res/layout/activity_entry_edit.xml index 67ba1771c..14ed20bd8 100644 --- a/app/src/main/res/layout/activity_entry_edit.xml +++ b/app/src/main/res/layout/activity_entry_edit.xml @@ -84,7 +84,7 @@ android:layout_height="?attr/actionBarSize" android:theme="?attr/toolbarActionAppearance" android:layout_gravity="bottom" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" /> @@ -105,7 +105,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/> - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/activity_file_selection.xml b/app/src/main/res/layout/activity_file_selection.xml index f1d42d78f..96448a944 100644 --- a/app/src/main/res/layout/activity_file_selection.xml +++ b/app/src/main/res/layout/activity_file_selection.xml @@ -146,7 +146,7 @@ android:id="@+id/file_selection_buttons_container" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> - \ No newline at end of file + + diff --git a/app/src/main/res/layout/activity_group.xml b/app/src/main/res/layout/activity_group.xml index cdeed30e1..9701e0534 100644 --- a/app/src/main/res/layout/activity_group.xml +++ b/app/src/main/res/layout/activity_group.xml @@ -27,7 +27,7 @@ android:filterTouchesWhenObscured="true" android:fitsSystemWindows="true"> - @@ -36,16 +36,17 @@ android:id="@+id/special_mode_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:theme="?attr/toolbarSpecialAppearance" /> + android:theme="?attr/toolbarSpecialAppearance" + app:layout_constraintTop_toTopOf="parent" /> + android:theme="?attr/toolbarAppearance" + android:title="@string/app_name" + app:layout_constraintTop_toBottomOf="@+id/special_mode_view"> - + + + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/toolbar_action" + app:layout_constraintTop_toBottomOf="@+id/toolbar"> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" /> - + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_icon_picker.xml b/app/src/main/res/layout/activity_icon_picker.xml index fee7d16f7..a56c04c8d 100644 --- a/app/src/main/res/layout/activity_icon_picker.xml +++ b/app/src/main/res/layout/activity_icon_picker.xml @@ -43,7 +43,7 @@ android:layout_height="?attr/actionBarSize" android:layout_gravity="bottom" android:theme="?attr/toolbarActionAppearance" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/toolbar" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" + app:layout_constraintStart_toStartOf="parent" /> + + diff --git a/app/src/main/res/layout/activity_image_viewer.xml b/app/src/main/res/layout/activity_image_viewer.xml index a600a7c0a..d9eb93679 100644 --- a/app/src/main/res/layout/activity_image_viewer.xml +++ b/app/src/main/res/layout/activity_image_viewer.xml @@ -14,7 +14,7 @@ - \ No newline at end of file + + + diff --git a/app/src/main/res/layout/activity_key_generator.xml b/app/src/main/res/layout/activity_key_generator.xml index 30f598dab..2ac63a461 100644 --- a/app/src/main/res/layout/activity_key_generator.xml +++ b/app/src/main/res/layout/activity_key_generator.xml @@ -43,7 +43,7 @@ android:layout_height="?attr/actionBarSize" android:layout_gravity="bottom" android:theme="?attr/toolbarActionAppearance" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" /> @@ -64,5 +64,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" /> + + diff --git a/app/src/main/res/layout/activity_main_credential.xml b/app/src/main/res/layout/activity_main_credential.xml index 84ae78ce4..4e968b227 100644 --- a/app/src/main/res/layout/activity_main_credential.xml +++ b/app/src/main/res/layout/activity_main_credential.xml @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - + app:layout_constraintTop_toBottomOf="@+id/special_mode_view" + app:layout_constraintBottom_toTopOf="@+id/activity_password_footer"> + app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"> - \ No newline at end of file + + diff --git a/app/src/main/res/layout/activity_toolbar.xml b/app/src/main/res/layout/activity_toolbar.xml index fff49771c..b11a3bac7 100644 --- a/app/src/main/res/layout/activity_toolbar.xml +++ b/app/src/main/res/layout/activity_toolbar.xml @@ -17,15 +17,22 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - + + + + - \ No newline at end of file + + diff --git a/app/src/main/res/layout/fragment_progress.xml b/app/src/main/res/layout/fragment_progress.xml index b3f79e265..6edd632d2 100644 --- a/app/src/main/res/layout/fragment_progress.xml +++ b/app/src/main/res/layout/fragment_progress.xml @@ -58,6 +58,16 @@ android:layout_marginEnd="20dp" style="@style/KeepassDXStyle.TextAppearance.Warning"/> +