From 2e0081b66cb8a95598b92411a676c2f273b57c72 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sat, 26 Feb 2022 12:51:00 +0100 Subject: [PATCH 001/171] Prepare hardware key in main credential --- .../AssignMainCredentialInDatabaseRunnable.kt | 3 +- .../keepass/database/element/Database.kt | 66 ++++++++++++------- .../database/element/database/DatabaseKDB.kt | 18 ++--- .../database/element/database/DatabaseKDBX.kt | 18 ++--- .../element/database/DatabaseVersioned.kt | 30 ++++++--- .../keepass/model/CipherEncryptDatabase.kt | 5 -- .../kunzisoft/keepass/model/MainCredential.kt | 64 ++++++++++++++++-- .../java/com/kunzisoft/encrypt/HashManager.kt | 10 +-- 8 files changed, 142 insertions(+), 72 deletions(-) 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..f1be79ea1 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 @@ -42,8 +42,7 @@ open class AssignMainCredentialInDatabaseRunnable ( 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.assignMasterKey(context.contentResolver, mMainCredential) } catch (e: Exception) { erase(mBackupKey) setError(e) 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 004906cdf..a31f4d45c 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 @@ -604,6 +604,20 @@ class Database { } } + @Throws(Exception::class) + private fun getKeyFileData(contentResolver: ContentResolver, keyFileUri: Uri?): ByteArray? { + try { + keyFileUri?.let { uri -> + UriUtil.getUriInputStream(contentResolver, uri)?.use { keyFileInputStream -> + return keyFileInputStream.readBytes() + } + } + } catch (e: OutOfMemoryError) { + throw LoadDatabaseException("Keyfile too large") + } + return null + } + @Throws(LoadDatabaseException::class) fun loadData(uri: Uri, mainCredential: MainCredential, @@ -620,14 +634,7 @@ class Database { // 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, { databaseInputStream -> @@ -641,7 +648,8 @@ class Database { ) { databaseKDB.retrieveMasterKey( mainCredential.masterPassword, - keyFileInputStream + getKeyFileData(contentResolver, mainCredential.keyFileUri), + mainCredential.hardwareKeyData ) } databaseKDB @@ -657,7 +665,8 @@ class Database { progressTaskUpdater) { databaseKDBX.retrieveMasterKey( mainCredential.masterPassword, - keyFileInputStream, + getKeyFileData(contentResolver, mainCredential.keyFileUri), + mainCredential.hardwareKeyData ) } } @@ -671,7 +680,6 @@ class Database { } catch (e: Exception) { throw LoadDatabaseException(e) } finally { - keyFileInputStream?.close() dataModifiedSinceLastLoading = false } } @@ -695,18 +703,9 @@ class Database { 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, { databaseInputStream -> val databaseToMergeKDB = DatabaseKDB() @@ -715,7 +714,11 @@ class Database { if (databaseToMergeMainCredential != null) { databaseToMergeKDB.retrieveMasterKey( databaseToMergeMainCredential.masterPassword, - keyFileInputStream, + getKeyFileData( + contentResolver, + databaseToMergeMainCredential.keyFileUri, + ), + databaseToMergeMainCredential.hardwareKeyData ) } else { databaseToMergeKDB.masterKey = masterKey @@ -731,7 +734,11 @@ class Database { if (databaseToMergeMainCredential != null) { databaseToMergeKDBX.retrieveMasterKey( databaseToMergeMainCredential.masterPassword, - keyFileInputStream, + getKeyFileData( + contentResolver, + databaseToMergeMainCredential.keyFileUri + ), + databaseToMergeMainCredential.hardwareKeyData ) } else { databaseToMergeKDBX.masterKey = masterKey @@ -769,7 +776,6 @@ class Database { } catch (e: Exception) { throw LoadDatabaseException(e) } finally { - keyFileInputStream?.close() databaseToMerge.clearAndClose() } } @@ -1024,9 +1030,19 @@ class Database { } @Throws(IOException::class) - fun assignMasterKey(key: String?, keyInputStream: InputStream?) { - mDatabaseKDB?.retrieveMasterKey(key, keyInputStream) - mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream) + fun assignMasterKey(contentResolver: ContentResolver, + mainCredential: MainCredential) { + val keyFileData = getKeyFileData(contentResolver, mainCredential.keyFileUri) + mDatabaseKDB?.retrieveMasterKey( + mainCredential.masterPassword, + keyFileData, + mainCredential.hardwareKeyData + ) + mDatabaseKDBX?.retrieveMasterKey( + mainCredential.masterPassword, + keyFileData, + mainCredential.hardwareKeyData + ) mDatabaseKDBX?.keyLastChanged = DateInstant() } 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..cc4f3a3f5 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 @@ -32,7 +32,6 @@ 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 java.io.IOException -import java.io.InputStream import java.util.* class DatabaseKDB : DatabaseVersioned() { @@ -117,14 +116,15 @@ class DatabaseKDB : DatabaseVersioned() { } @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) + override fun getMasterKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKey: ByteArray?): ByteArray { + return if (passwordKey != null && keyFileData != null) { + getCompositeKey(passwordKey, keyFileData, null) ?: byteArrayOf() + } else if (passwordKey != null) { // key.length() >= 0 + getPasswordKey(passwordKey) + } else if (keyFileData != null) { // key == null + getFileKey(keyFileData) } else { throw IllegalArgumentException("Key cannot be empty.") } 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 adc05d675..03d0a41c1 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 @@ -529,19 +529,11 @@ class DatabaseKDBX : DatabaseVersioned { } @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) + public override fun getMasterKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKey: ByteArray?): ByteArray { + return getCompositeKey(passwordKey, keyFileData, hardwareKey) + ?: HashManager.hashSha256(byteArrayOf()) } @Throws(IOException::class) 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..fc30bec30 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 @@ -92,18 +92,30 @@ abstract class DatabaseVersioned< } @Throws(IOException::class) - protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray + protected abstract fun getMasterKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKey: ByteArray?): ByteArray @Throws(IOException::class) - fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) { - masterKey = getMasterKey(key, keyfileInputStream) + fun retrieveMasterKey(key: String?, + keyFileData: ByteArray?, + hardwareKeyData: ByteArray?) { + masterKey = getMasterKey(key, keyFileData, hardwareKeyData) } @Throws(IOException::class) - protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray { - val fileKey = getFileKey(keyfileInputStream) - val passwordKey = getPasswordKey(key) - return HashManager.hashSha256(passwordKey, fileKey) + protected fun getCompositeKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKeyData: ByteArray?): ByteArray? { + if (passwordKey == null && keyFileData == null && hardwareKeyData == null) + return null + val passwordBytes = if (passwordKey != null) getPasswordKey(passwordKey) else null + val keyFileBytes = if (keyFileData != null) getFileKey(keyFileData) else null + return HashManager.hashSha256( + passwordBytes, + keyFileBytes, + hardwareKeyData + ) } @Throws(IOException::class) @@ -117,10 +129,8 @@ abstract class DatabaseVersioned< } @Throws(IOException::class) - protected fun getFileKey(keyInputStream: InputStream): ByteArray { + protected fun getFileKey(keyData: ByteArray): ByteArray { try { - val keyData = keyInputStream.readBytes() - // Check XML key file val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) if (xmlKeyByteArray != null) { 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/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt index e5ee12c72..fc2f31eee 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt @@ -1,25 +1,81 @@ +/* + * 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.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 { +data class MainCredential(var masterPassword: String? = null, + var keyFileUri: Uri? = null, + var hardwareKeyData: ByteArray? = null): Parcelable { - constructor(parcel: Parcel) : this( - parcel.readString(), - parcel.readParcelable(Uri::class.java.classLoader)) { + constructor(parcel: Parcel) : this() { + masterPassword = parcel.readString() + keyFileUri = parcel.readParcelable(Uri::class.java.classLoader) + val hardwareKeyDataLength = parcel.readInt() + if (hardwareKeyDataLength >= 0) { + hardwareKeyData = ByteArray(hardwareKeyDataLength) + parcel.readByteArray(hardwareKeyData!!) + } else { + hardwareKeyData = null + } } override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(masterPassword) parcel.writeParcelable(keyFileUri, flags) + if (hardwareKeyData != null) { + parcel.writeInt(hardwareKeyData!!.size) + parcel.writeByteArray(hardwareKeyData) + } else { + parcel.writeInt(-1) + } } 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 (masterPassword != other.masterPassword) return false + if (keyFileUri != other.keyFileUri) return false + if (hardwareKeyData != null) { + if (other.hardwareKeyData == null) return false + if (!hardwareKeyData.contentEquals(other.hardwareKeyData)) return false + } else if (other.hardwareKeyData != null) return false + + return true + } + + override fun hashCode(): Int { + var result = masterPassword?.hashCode() ?: 0 + result = 31 * result + (keyFileUri?.hashCode() ?: 0) + result = 31 * result + (hardwareKeyData?.contentHashCode() ?: 0) + return result + } + companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): MainCredential { return MainCredential(parcel) diff --git a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt index cde50b24e..1c7e1e573 100644 --- a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt +++ b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt @@ -39,10 +39,11 @@ object HashManager { return messageDigest } - fun hashSha256(vararg data: ByteArray): ByteArray { + fun hashSha256(vararg data: ByteArray?): ByteArray { val hash: MessageDigest = getHash256() for (byteArray in data) { - hash.update(byteArray) + if (byteArray != null) + hash.update(byteArray) } return hash.digest() } @@ -57,10 +58,11 @@ object HashManager { return messageDigest } - private fun hashSha512(vararg data: ByteArray): ByteArray { + private fun hashSha512(vararg data: ByteArray?): ByteArray { val hash: MessageDigest = getHash512() for (byteArray in data) { - hash.update(byteArray) + if (byteArray != null) + hash.update(byteArray) } return hash.digest() } From 279bd16b744ad6871b0a7d62f36347f8618bc820 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sat, 26 Feb 2022 13:13:07 +0100 Subject: [PATCH 002/171] Best autofill recognition #1250 --- CHANGELOG | 1 + .../com/kunzisoft/keepass/autofill/StructureParser.kt | 8 +++++--- fastlane/metadata/android/en-US/changelogs/103.txt | 3 ++- fastlane/metadata/android/fr-FR/changelogs/103.txt | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0fe45cf8c..56efc01f4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ KeePassDX(3.3.1) * Better OOM management #256 * Fix filters #1249 * Fix temp advanced unlocking #1245 + * Best autofill recognition #1250 KeePassDX(3.3.0) * Quick search and dynamic filters #163 #462 #521 diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index 760a09e92..d3378f90d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -377,9 +377,11 @@ class StructureParser(private val structure: AssistStructure) { when { inputIsVariationType(inputType, InputType.TYPE_NUMBER_VARIATION_NORMAL) -> { - usernameIdCandidate = autofillId - usernameValueCandidate = node.autofillValue - Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}") + if (usernameIdCandidate == null) { + usernameIdCandidate = autofillId + usernameValueCandidate = node.autofillValue + Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}") + } } inputIsVariationType(inputType, InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> { diff --git a/fastlane/metadata/android/en-US/changelogs/103.txt b/fastlane/metadata/android/en-US/changelogs/103.txt index 34e29d4d5..f1d5a8db5 100644 --- a/fastlane/metadata/android/en-US/changelogs/103.txt +++ b/fastlane/metadata/android/en-US/changelogs/103.txt @@ -1,4 +1,5 @@ * Fix Japanese keyboard in search #1248 * Better OOM management #256 * Fix filters #1249 - * Fix temp advanced unlocking #1245 \ No newline at end of file + * Fix temp advanced unlocking #1245 + * Best autofill recognition #1250 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/103.txt b/fastlane/metadata/android/fr-FR/changelogs/103.txt index 0e4721288..ba8745a62 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/103.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/103.txt @@ -1,4 +1,5 @@ * Correction du clavier japonais dans la recherche #1248 * Meilleur gestion d'OOM #256 * Correction des filtres #1249 - * Correction du déverouillage avancé temporaire #1245 \ No newline at end of file + * Correction du déverouillage avancé temporaire #1245 + * Meilleure reconnaissance du remplissage automatique #1250 \ No newline at end of file From ecbee73eae3b583be68e9674362c813ee3b08762 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 21 Apr 2022 18:03:32 +0200 Subject: [PATCH 003/171] Add view and first implementation of hardware key #8 --- .../activities/MainCredentialActivity.kt | 60 +++++++- .../dialogs/MainCredentialDialogFragment.kt | 2 +- .../SetMainCredentialDialogFragment.kt | 2 +- .../kunzisoft/keepass/hardware/HardwareKey.kt | 33 +++++ .../hardware/HardwareKeyResponseHelper.kt | 125 +++++++++++++++++ .../keepass/view/HardwareKeySelectionView.kt | 130 ++++++++++++++++++ .../keepass/view/MainCredentialView.kt | 65 ++++++--- .../layout/fragment_set_main_credential.xml | 35 ++++- .../layout/view_hardware_key_selection.xml | 26 ++++ .../main/res/layout/view_main_credentials.xml | 34 ++++- app/src/main/res/values/strings.xml | 2 + 11 files changed, 482 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt create mode 100644 app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt create mode 100644 app/src/main/res/layout/view_hardware_key_selection.xml 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 bc4e50870..3c536a7c6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -59,6 +59,8 @@ import com.kunzisoft.keepass.database.element.Database 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.hardware.HardwareKeyResponseHelper 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 @@ -101,6 +103,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private var mRememberKeyFile: Boolean = false private var mExternalFileHelper: ExternalFileHelper? = null + private var mHardwareKeyResponseHelper: HardwareKeyResponseHelper? = null + private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false @@ -134,10 +138,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu } mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(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) @@ -145,6 +150,35 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu loadDatabase() } + // Build elements to manage hardware key + mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) + mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData -> + mainCredentialView?.challengeResponse = responseData + } + mainCredentialView?.onRequestHardwareKeyResponse = { hardwareKey -> + try { + when (hardwareKey) { + HardwareKey.HMAC_SHA1_KPXC -> { + mDatabaseFileUri?.let { databaseUri -> + mHardwareKeyResponseHelper?.launchChallengeForResponse(databaseUri) + } + } + else -> { + // TODO other algorithm + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve the challenge response", e) + e.message?.let { message -> + Snackbar.make( + coordinatorLayout, + message, + Snackbar.LENGTH_LONG + ).asError().show() + } + } + } + // If is a view intent getUriFromIntent(intent) @@ -171,6 +205,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 -> @@ -335,11 +379,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu if (action != null && action == VIEW_INTENT) { mDatabaseFileUri = intent.data - mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) + mainCredentialView?.populateKeyFileView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) } else { mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) intent?.getParcelableExtra(KEY_KEYFILE)?.let { - mainCredentialView?.populateKeyFileTextView(it) + mainCredentialView?.populateKeyFileView(it) } } try { @@ -436,11 +480,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { // Define Key File text if (mRememberKeyFile) { - mainCredentialView?.populateKeyFileTextView(keyFileUri) + mainCredentialView?.populateKeyFileView(keyFileUri) } // 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 @@ -475,7 +521,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { mainCredentialView?.populatePasswordTextView(null) if (clearKeyFile) { - mainCredentialView?.populateKeyFileTextView(null) + mainCredentialView?.populateKeyFileView(null) } } 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..2180caa8a 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 @@ -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/SetMainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt index e654c0817..661cdd22e 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 @@ -138,7 +138,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { passwordRepeatView = rootView?.findViewById(R.id.password_confirmation) passwordRepeatView?.applyFontVisibility() - keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) + keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkbox) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) mExternalFileHelper = ExternalFileHelper(this) 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..f483857c1 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt @@ -0,0 +1,33 @@ +package com.kunzisoft.keepass.hardware + +enum class HardwareKey(val value: String) { + HMAC_SHA1_KPXC("HMAC-SHA1 KPXC"), + HMAC_SHA1_KP2("HMAC-SHA1 KP2"), + OATH_HOTP("OATH HOTP"), + HMAC_SECRET_FIDO2("HMAC-SECRET FIDO2"); + + companion object { + val DEFAULT = HMAC_SHA1_KPXC + + fun getStringValues(): List { + return values().map { it.value } + } + + fun fromPosition(position: Int): HardwareKey { + return when (position) { + 0 -> HMAC_SHA1_KPXC + 1 -> HMAC_SHA1_KP2 + 2 -> OATH_HOTP + 3 -> HMAC_SECRET_FIDO2 + else -> DEFAULT + } + } + + fun getHardwareKeyFromString(text: String): HardwareKey { + 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/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt new file mode 100644 index 000000000..e501670bb --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt @@ -0,0 +1,125 @@ +package com.kunzisoft.keepass.hardware + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +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.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX +import com.kunzisoft.keepass.utils.UriUtil +import java.io.BufferedInputStream +import java.io.IOException +import java.io.InputStream + +class HardwareKeyResponseHelper { + + private var activity: FragmentActivity? = null + private var fragment: Fragment? = null + + private var getChallengeResponseResultLauncher: ActivityResultLauncher? = null + + constructor(context: FragmentActivity) { + this.activity = context + this.fragment = null + } + + constructor(context: Fragment) { + this.activity = context.activity + this.fragment = context + } + + fun buildHardwareKeyResponse(onChallengeResponded: ((challengeResponse:ByteArray?) -> Unit)?) { + val resultCallback = ActivityResultCallback { result -> + Log.d(TAG, "resultCode from ykdroid: " + result.resultCode) + if (result.resultCode == Activity.RESULT_OK) { + val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response") + Log.d(TAG, "Response: " + challengeResponse.contentToString()) + challengeResponse?.let { + onChallengeResponded?.invoke(challengeResponse) + } + } + } + + getChallengeResponseResultLauncher = if (fragment != null) { + fragment?.registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + resultCallback + ) + } else { + activity?.registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + resultCallback + ) + } + } + + fun launchChallengeForResponse(databaseUri: Uri) { + fragment?.context?.contentResolver ?: activity?.contentResolver ?.let { contentResolver -> + getTransformSeedFromHeader(databaseUri, contentResolver)?.let { seed -> + // seed: 32 byte transform seed, needs to be padded before sent to the hardware + val challenge = ByteArray(64) + System.arraycopy(seed, 0, challenge, 0, 32) + challenge.fill(32, 32, 64) + val intent = Intent("net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE") + Log.d(TAG, "Challenge sent to yubikey: " + challenge.contentToString()) + intent.putExtra("challenge", challenge) + try { + getChallengeResponseResultLauncher?.launch(intent) + } catch (e: ActivityNotFoundException) { + // TODO better error + throw IOException("No activity to handle CHALLENGE_RESPONSE intent") + } + } + } + } + + private fun getTransformSeedFromHeader(uri: Uri, contentResolver: ContentResolver): ByteArray? { + // TODO better implementation + var databaseInputStream: InputStream? = null + var challenge: ByteArray? = 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) + + // Return to the start + databaseInputStream.reset() + + val header = DatabaseHeaderKDBX(DatabaseKDBX()) + + header.loadFromFile(databaseInputStream) + + challenge = ByteArray(64) + System.arraycopy(header.transformSeed, 0, challenge, 0, 32) + challenge.fill(32, 32, 64) + + } catch (e: Exception) { + Log.e(TAG, "Could not read transform seed from file") + } finally { + databaseInputStream?.close() + } + + return challenge + } + + companion object { + private val TAG = HardwareKeyResponseHelper::class.java.simpleName + } +} \ No newline at end of file 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..88cfddf0b --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt @@ -0,0 +1,130 @@ +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.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 = HardwareKey.DEFAULT + + 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) + + hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion) + + hardwareKeyCompletion.inputType = InputType.TYPE_NULL + hardwareKeyCompletion.setAdapter(mHardwareKeyAdapter) + + hardwareKeyCompletion.onItemClickListener = + AdapterView.OnItemClickListener { _, _, position, _ -> + mHardwareKey = HardwareKey.fromPosition(position) + selectionListener?.invoke(mHardwareKey) + } + } + + var hardwareKey: HardwareKey + get() { + return mHardwareKey + } + set(value) { + mHardwareKey = value + hardwareKeyCompletion.setSelection(value.ordinal) + } + + 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 = HardwareKey.DEFAULT + + constructor(superState: Parcelable?) : super(superState) + + private constructor(parcel: Parcel) : super(parcel) { + mHardwareKey = parcel.readEnum() ?: HardwareKey.DEFAULT + } + + 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..11022f526 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -30,14 +30,12 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.widget.CompoundButton -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.TextView +import android.widget.* import androidx.appcompat.app.AppCompatActivity import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.model.MainCredential @@ -46,13 +44,18 @@ class MainCredentialView @JvmOverloads constructor(context: Context, 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 + var onRequestHardwareKeyResponse: ((HardwareKey)-> Unit)? = null private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD @@ -60,15 +63,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) - passwordTextView = findViewById(R.id.password_text_view) - keyFileSelectionView = findViewById(R.id.keyfile_selection) checkboxPasswordView = findViewById(R.id.password_checkbox) - checkboxKeyFileView = findViewById(R.id.keyfile_checkox) + passwordTextView = findViewById(R.id.password_text_view) + checkboxKeyFileView = findViewById(R.id.keyfile_checkbox) + keyFileSelectionView = findViewById(R.id.keyfile_selection) + 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 +96,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 +105,25 @@ class MainCredentialView @JvmOverloads constructor(context: Context, checkboxPasswordView.setOnCheckedChangeListener { view, checked -> onPasswordChecked?.onCheckedChanged(view, checked) } + checkboxKeyFileView.setOnCheckedChangeListener { view, checked -> + onKeyFileChecked?.onCheckedChanged(view, checked) + } + checkboxHardwareView.setOnCheckedChangeListener { view, checked -> + onHardwareKeyChecked?.onCheckedChanged(view, checked) + } + + hardwareKeySelectionView.selectionListener = { _ -> + checkboxHardwareView.isChecked = true + } } - fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { - keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) + fun validateCredential() { + val hardwareKey = hardwareKeySelectionView.hardwareKey + if (checkboxHardwareView.isChecked) { + onRequestHardwareKeyResponse?.invoke(hardwareKey) + } else { + onValidateListener?.invoke() + } } fun populatePasswordTextView(text: String?) { @@ -118,7 +138,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 +150,27 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } - fun isFill(): Boolean { - return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked + fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { + keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) } + fun isFill(): Boolean { + return checkboxPasswordView.isChecked + || checkboxKeyFileView.isChecked // TODO better recognition + || checkboxHardwareView.isChecked + } + + // TODO Challenge response + var challengeResponse: ByteArray? = null + fun getMainCredential(): MainCredential { return MainCredential().apply { this.masterPassword = if (checkboxPasswordView.isChecked) passwordTextView.text?.toString() else null this.keyFileUri = if (checkboxKeyFileView.isChecked) keyFileSelectionView.uri else null + this.hardwareKeyData = if (checkboxHardwareView.isChecked) + challengeResponse else null } } @@ -151,7 +182,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/res/layout/fragment_set_main_credential.xml b/app/src/main/res/layout/fragment_set_main_credential.xml index 343124bdb..b17de8a52 100644 --- a/app/src/main/res/layout/fragment_set_main_credential.xml +++ b/app/src/main/res/layout/fragment_set_main_credential.xml @@ -115,7 +115,7 @@ android:orientation="vertical"> @@ -126,7 +126,38 @@ android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@+id/keyfile_checkox" + app:layout_constraintStart_toEndOf="@+id/keyfile_checkbox" + app:layout_constraintEnd_toEndOf="parent" /> + + + + + + + + + diff --git a/app/src/main/res/layout/view_hardware_key_selection.xml b/app/src/main/res/layout/view_hardware_key_selection.xml new file mode 100644 index 000000000..2c441ef5e --- /dev/null +++ b/app/src/main/res/layout/view_hardware_key_selection.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_main_credentials.xml b/app/src/main/res/layout/view_main_credentials.xml index e0f9ff64e..41d632516 100644 --- a/app/src/main/res/layout/view_main_credentials.xml +++ b/app/src/main/res/layout/view_main_credentials.xml @@ -62,7 +62,7 @@ android:layout_height="wrap_content"> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 556135f33..adf076d03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ One-time password info Password checkbox Keyfile checkbox + Hardware key checkbox Repeat toggle password visibility Entry icon Database color @@ -96,6 +97,7 @@ History Attachments Keyfile + Hardware key Modified Searchable Inherit From e8f79ae467a4c5fd318df91cbe3542f0586d526a Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Mon, 25 Apr 2022 21:47:43 +0200 Subject: [PATCH 004/171] Fix to open challenge-response dynamically / refactoring methods #8 --- .../activities/MainCredentialActivity.kt | 2 +- .../database/element/database/DatabaseKDB.kt | 12 ++++---- .../database/element/database/DatabaseKDBX.kt | 8 ++--- .../element/database/DatabaseVersioned.kt | 30 +++++++++++-------- .../keepass/view/MainCredentialView.kt | 13 ++++---- 5 files changed, 36 insertions(+), 29 deletions(-) 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 3c536a7c6..e7bbb5641 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -153,7 +153,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu // Build elements to manage hardware key mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData -> - mainCredentialView?.challengeResponse = responseData + mainCredentialView?.validateCredential(responseData) } mainCredentialView?.onRequestHardwareKeyResponse = { hardwareKey -> try { 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 cc4f3a3f5..48a3a2df8 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 @@ -116,15 +116,15 @@ class DatabaseKDB : DatabaseVersioned() { } @Throws(IOException::class) - override fun getMasterKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKey: ByteArray?): ByteArray { + override fun deriveMasterKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKey: ByteArray?): ByteArray { return if (passwordKey != null && keyFileData != null) { - getCompositeKey(passwordKey, keyFileData, null) ?: byteArrayOf() + retrieveCompositeKey(passwordKey, keyFileData, null) ?: byteArrayOf() } else if (passwordKey != null) { // key.length() >= 0 - getPasswordKey(passwordKey) + retrievePasswordKey(passwordKey) } else if (keyFileData != null) { // key == null - getFileKey(keyFileData) + retrieveFileKey(keyFileData) } else { throw IllegalArgumentException("Key cannot be empty.") } 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 03d0a41c1..b95cf599d 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 @@ -529,10 +529,10 @@ class DatabaseKDBX : DatabaseVersioned { } @Throws(IOException::class) - public override fun getMasterKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKey: ByteArray?): ByteArray { - return getCompositeKey(passwordKey, keyFileData, hardwareKey) + public override fun deriveMasterKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKey: ByteArray?): ByteArray { + return retrieveCompositeKey(passwordKey, keyFileData, hardwareKey) ?: HashManager.hashSha256(byteArrayOf()) } 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 fc30bec30..cded530c7 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 @@ -92,34 +92,35 @@ abstract class DatabaseVersioned< } @Throws(IOException::class) - protected abstract fun getMasterKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKey: ByteArray?): ByteArray + protected abstract fun deriveMasterKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKey: ByteArray?): ByteArray @Throws(IOException::class) fun retrieveMasterKey(key: String?, keyFileData: ByteArray?, hardwareKeyData: ByteArray?) { - masterKey = getMasterKey(key, keyFileData, hardwareKeyData) + masterKey = deriveMasterKey(key, keyFileData, hardwareKeyData) } @Throws(IOException::class) - protected fun getCompositeKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKeyData: ByteArray?): ByteArray? { + protected fun retrieveCompositeKey(passwordKey: String?, + keyFileData: ByteArray?, + hardwareKeyData: ByteArray?): ByteArray? { if (passwordKey == null && keyFileData == null && hardwareKeyData == null) return null - val passwordBytes = if (passwordKey != null) getPasswordKey(passwordKey) else null - val keyFileBytes = if (keyFileData != null) getFileKey(keyFileData) else null + val passwordBytes = if (passwordKey != null) retrievePasswordKey(passwordKey) else null + val keyFileBytes = if (keyFileData != null) retrieveFileKey(keyFileData) else null + val hardwareKeyBytes = if (hardwareKeyData != null) retrieveHardwareKey(hardwareKeyData) else null return HashManager.hashSha256( passwordBytes, keyFileBytes, - hardwareKeyData + hardwareKeyBytes ) } @Throws(IOException::class) - protected fun getPasswordKey(key: String): ByteArray { + protected fun retrievePasswordKey(key: String): ByteArray { val bKey: ByteArray = try { key.toByteArray(charset(passwordEncoding)) } catch (e: UnsupportedEncodingException) { @@ -129,7 +130,7 @@ abstract class DatabaseVersioned< } @Throws(IOException::class) - protected fun getFileKey(keyData: ByteArray): ByteArray { + protected fun retrieveFileKey(keyData: ByteArray): ByteArray { try { // Check XML key file val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) @@ -153,6 +154,11 @@ abstract class DatabaseVersioned< } } + @Throws(IOException::class) + protected fun retrieveHardwareKey(keyData: ByteArray): ByteArray { + return HashManager.hashSha256(keyData) + } + protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { return null } 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 11022f526..ea75c4504 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -57,6 +57,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context, var onValidateListener: (() -> Unit)? = null var onRequestHardwareKeyResponse: ((HardwareKey)-> Unit)? = null + private var mChallengeResponse: ByteArray? = null + private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD init { @@ -117,12 +119,14 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } - fun validateCredential() { + fun validateCredential(hardwareKeyData: ByteArray? = null) { val hardwareKey = hardwareKeySelectionView.hardwareKey - if (checkboxHardwareView.isChecked) { + if (hardwareKeyData == null && checkboxHardwareView.isChecked) { onRequestHardwareKeyResponse?.invoke(hardwareKey) } else { + mChallengeResponse = hardwareKeyData onValidateListener?.invoke() + mChallengeResponse = null } } @@ -160,9 +164,6 @@ class MainCredentialView @JvmOverloads constructor(context: Context, || checkboxHardwareView.isChecked } - // TODO Challenge response - var challengeResponse: ByteArray? = null - fun getMainCredential(): MainCredential { return MainCredential().apply { this.masterPassword = if (checkboxPasswordView.isChecked) @@ -170,7 +171,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, this.keyFileUri = if (checkboxKeyFileView.isChecked) keyFileSelectionView.uri else null this.hardwareKeyData = if (checkboxHardwareView.isChecked) - challengeResponse else null + mChallengeResponse else null } } From 5b4338abaec322bed3594927dfd4f571579606d8 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 27 Apr 2022 14:39:08 +0200 Subject: [PATCH 005/171] Better implementation for challenge response intent --- .../hardware/HardwareKeyResponseHelper.kt | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt index e501670bb..a0efa42e1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt @@ -64,62 +64,41 @@ class HardwareKeyResponseHelper { fun launchChallengeForResponse(databaseUri: Uri) { fragment?.context?.contentResolver ?: activity?.contentResolver ?.let { contentResolver -> getTransformSeedFromHeader(databaseUri, contentResolver)?.let { seed -> - // seed: 32 byte transform seed, needs to be padded before sent to the hardware - val challenge = ByteArray(64) - System.arraycopy(seed, 0, challenge, 0, 32) - challenge.fill(32, 32, 64) - val intent = Intent("net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE") - Log.d(TAG, "Challenge sent to yubikey: " + challenge.contentToString()) - intent.putExtra("challenge", challenge) try { - getChallengeResponseResultLauncher?.launch(intent) + getChallengeResponseResultLauncher?.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply { + putExtra(YKDROID_SEED_KEY, seed) + }) + Log.d(TAG, "Challenge sent : " + seed.contentToString()) } catch (e: ActivityNotFoundException) { // TODO better error - throw IOException("No activity to handle CHALLENGE_RESPONSE intent") + throw IOException("No activity to handle $YKDROID_CHALLENGE_RESPONSE_INTENT intent") } } } } private fun getTransformSeedFromHeader(uri: Uri, contentResolver: ContentResolver): ByteArray? { - // TODO better implementation - var databaseInputStream: InputStream? = null - var challenge: ByteArray? = 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.") + BufferedInputStream(UriUtil.getUriInputStream(contentResolver, uri)).use { databaseInputStream -> + val header = DatabaseHeaderKDBX(DatabaseKDBX()) + header.loadFromFile(databaseInputStream) + val challenge = ByteArray(64) + header.transformSeed?.copyInto(challenge, 0, 0, 32) + // seed: 32 byte transform seed, needs to be padded before sent to the hardware + challenge.fill(32, 32, 64) + return challenge } - - // We'll end up reading 8 bytes to identify the header. Might as well use two extra. - databaseInputStream.mark(10) - - // Return to the start - databaseInputStream.reset() - - val header = DatabaseHeaderKDBX(DatabaseKDBX()) - - header.loadFromFile(databaseInputStream) - - challenge = ByteArray(64) - System.arraycopy(header.transformSeed, 0, challenge, 0, 32) - challenge.fill(32, 32, 64) - } catch (e: Exception) { - Log.e(TAG, "Could not read transform seed from file") - } finally { - databaseInputStream?.close() + Log.e(TAG, "Could not read transform seed from file", e) } - - return challenge + return null } companion object { private val TAG = HardwareKeyResponseHelper::class.java.simpleName + + private const val YKDROID_CHALLENGE_RESPONSE_INTENT = "net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" + private const val YKDROID_SEED_KEY = "challenge" + } } \ No newline at end of file From b44c9cfc51f5d3ce411d997cbb563fd260778738 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 28 Apr 2022 20:39:26 +0200 Subject: [PATCH 006/171] Opening refactoring --- .../activities/MainCredentialActivity.kt | 4 +- .../SetMainCredentialDialogFragment.kt | 190 ++++++++++------- .../activities/legacy/DatabaseLockActivity.kt | 1 - .../database/action/LoadDatabaseRunnable.kt | 26 +-- .../database/action/MergeDatabaseRunnable.kt | 5 +- .../keepass/database/element/Database.kt | 191 ++++++++++-------- .../kunzisoft/keepass/hardware/HardwareKey.kt | 14 +- .../hardware/HardwareKeyResponseHelper.kt | 46 ++--- .../kunzisoft/keepass/utils/ParcelableUtil.kt | 20 ++ .../layout/fragment_set_main_credential.xml | 4 +- 10 files changed, 284 insertions(+), 217 deletions(-) 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 e7bbb5641..c1e3a6921 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -152,13 +152,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu // Build elements to manage hardware key mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) - mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData -> + mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData, _ -> mainCredentialView?.validateCredential(responseData) } mainCredentialView?.onRequestHardwareKeyResponse = { hardwareKey -> try { when (hardwareKey) { - HardwareKey.HMAC_SHA1_KPXC -> { + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { mDatabaseFileUri?.let { databaseUri -> mHardwareKeyResponseHelper?.launchChallengeForResponse(databaseUri) } 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 661cdd22e..21b9a7a9f 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.hardware.HardwareKey +import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.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,22 @@ 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 mChallengeResponse: ByteArray? = null - private var rootView: View? = null + private lateinit var rootView: View - private var passwordCheckBox: CompoundButton? = null + private lateinit var passwordCheckBox: CompoundButton + private lateinit var passKeyView: 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 +74,17 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private var mNoKeyConfirmationDialog: AlertDialog? = null private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null + private var mHardwareKeyResponseHelper: HardwareKeyResponseHelper? = 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 +124,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,64 +138,59 @@ 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) + 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() - keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkbox) - keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) + keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox) + 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 = { _ -> + hardwareKeyCheckBox.isChecked = true + } 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 = "" + mKeyFileUri = null + mChallengeResponse = null - 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() + if (verifyHardwareKey()) { + approveMainCredential() } } + val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) + negativeButton.setOnClickListener { + mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential()) + dismiss() + } } return dialog @@ -194,69 +199,106 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { return super.onCreateDialog(savedInstanceState) } + private fun approveMainCredential() { + var error = verifyPassword() || verifyKeyFile() + if (!passwordCheckBox.isChecked + && !keyFileCheckBox.isChecked + && !hardwareKeyCheckBox.isChecked + ) { + error = true + if (mAllowNoMasterKey) + showNoKeyConfirmationDialog() + else { + passwordRepeatTextInputLayout.error = + getString(R.string.error_disallow_no_credentials) + } + } + if (!error) { + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) + dismiss() + } + } + + fun setChallengeResponse(response: ByteArray?) { + mChallengeResponse = response + approveMainCredential() + } + private fun retrieveMainCredential(): MainCredential { - val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null - val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null - return MainCredential(masterPassword, keyFile) + val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null + val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null + val hardwareKeyData = if (hardwareKeyCheckBox.isChecked) mChallengeResponse else null + return MainCredential(masterPassword, keyFileUri, hardwareKeyData) } override fun onResume() { super.onResume() // To check checkboxes if a text is present - passKeyView?.addTextChangedListener(passwordTextWatcher) + passKeyView.addTextChangedListener(passwordTextWatcher) } override fun onPause() { super.onPause() - passKeyView?.removeTextChangedListener(passwordTextWatcher) + passKeyView.removeTextChangedListener(passwordTextWatcher) } private fun verifyPassword(): Boolean { var error = false - if (passwordCheckBox != null - && passwordCheckBox!!.isChecked - && passKeyView != null - && passwordRepeatView != null) { - mMasterPassword = passKeyView!!.passwordString - val confPassword = passwordRepeatView!!.text.toString() + if (passwordCheckBox.isChecked) { + mMasterPassword = passKeyView.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) } - if ((mMasterPassword == null - || mMasterPassword!!.isEmpty()) - && (keyFileCheckBox == null - || !keyFileCheckBox!!.isChecked - || keyFileSelectionView?.uri == null)) { + if ((mMasterPassword.isNullOrEmpty()) + && (!keyFileCheckBox.isChecked + || keyFileSelectionView.uri == null)) { error = true showEmptyPasswordConfirmationDialog() } } - return error } private fun verifyKeyFile(): Boolean { var error = false - if (keyFileCheckBox != null - && keyFileCheckBox!!.isChecked) { - - keyFileSelectionView?.uri?.let { uri -> - mKeyFile = uri + if (keyFileCheckBox.isChecked) { + keyFileSelectionView.uri?.let { uri -> + mKeyFileUri = uri } ?: run { error = true - keyFileSelectionView?.error = getString(R.string.error_nokeyfile) + keyFileSelectionView.error = getString(R.string.error_nokeyfile) } } return error } + private fun verifyHardwareKey(): Boolean { + if (hardwareKeyCheckBox.isChecked) { + try { + when (hardwareKeySelectionView.hardwareKey) { + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { + // TODO Yubikey algorithm + } + else -> { + // TODO other algorithm + } + } + } catch (e: Exception) { + // TODO Log.e(TAG, "Unable to retrieve the challenge response", e) + } + return false + } + return true + } + private fun showEmptyPasswordConfirmationDialog() { activity?.let { val builder = AlertDialog.Builder(it) @@ -299,8 +341,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/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index 0e55420a7..14e28631e 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 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..fe0546f86 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 @@ -35,7 +35,7 @@ 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 mReadonly: Boolean, private val mCipherEncryptDatabase: CipherEncryptDatabase?, @@ -51,16 +51,18 @@ 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, + mReadonly, + UriUtil.getBinaryDir(context), + { memoryWanted -> + BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) + }, + mFixDuplicateUUID, + progressTaskUpdater + ) } catch (e: LoadDatabaseException) { setError(e) @@ -70,7 +72,7 @@ class LoadDatabaseRunnable(private val context: Context, // Save keyFile in app database if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context) - .addOrUpdateDatabaseUri(mUri, + .addOrUpdateDatabaseUri(mDatabaseUri, if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) } 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..aad8519ca 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 @@ -43,9 +43,10 @@ class MergeDatabaseRunnable(private val context: Context, override fun onActionRun() { try { - mDatabase.mergeData(mDatabaseToMergeUri, - mDatabaseToMergeMainCredential, + mDatabase.mergeData( context.contentResolver, + mDatabaseToMergeUri, + mDatabaseToMergeMainCredential, { memoryWanted -> BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) }, 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 acceef736..d3768c47e 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 @@ -73,7 +73,7 @@ class Database { var fileUri: Uri? = null private set - private var mSearchHelper: SearchHelper? = null + private var mSearchHelper: SearchHelper = SearchHelper() var isReadOnly = false @@ -557,53 +557,6 @@ class Database { 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(Exception::class) private fun getKeyFileData(contentResolver: ContentResolver, keyFileUri: Uri?): ByteArray? { try { @@ -612,31 +565,34 @@ class Database { return keyFileInputStream.readBytes() } } - } catch (e: OutOfMemoryError) { - throw LoadDatabaseException("Keyfile too large") + } catch (e: Exception) { + Log.e(TAG, "Unable to load the keyfile.", e) + throw LoadDatabaseException("Unable to load the keyfile.") } return null } @Throws(LoadDatabaseException::class) - fun loadData(uri: Uri, - mainCredential: MainCredential, - readOnly: Boolean, - contentResolver: ContentResolver, - cacheDirectory: File, - isRAMSufficient: (memoryWanted: Long) -> Boolean, - fixDuplicateUUID: Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { + fun loadData( + contentResolver: ContentResolver, + databaseUri: Uri, + mainCredential: MainCredential, + 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 try { // Read database stream for the first time - readDatabaseStream(contentResolver, uri, + readDatabaseStream(contentResolver, databaseUri, { databaseInputStream -> val databaseKDB = DatabaseKDB().apply { binaryCache.cacheDirectory = cacheDirectory @@ -652,7 +608,7 @@ class Database { mainCredential.hardwareKeyData ) } - databaseKDB + setDatabaseKDB(databaseKDB) }, { databaseInputStream -> val databaseKDBX = DatabaseKDBX().apply { @@ -670,11 +626,10 @@ class Database { ) } } - databaseKDBX + setDatabaseKDBX(databaseKDBX) } ) - } catch (e: FileNotFoundException) { - throw FileNotFoundDatabaseException("Unable to load the keyfile") + loaded = true } catch (e: LoadDatabaseException) { throw e } catch (e: Exception) { @@ -689,11 +644,13 @@ class Database { } @Throws(LoadDatabaseException::class) - fun mergeData(databaseToMergeUri: Uri?, - databaseToMergeMainCredential: MainCredential?, - contentResolver: ContentResolver, - isRAMSufficient: (memoryWanted: Long) -> Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { + fun mergeData( + contentResolver: ContentResolver, + databaseToMergeUri: Uri?, + databaseToMergeMainCredential: MainCredential?, + isRAMSufficient: (memoryWanted: Long) -> Boolean, + progressTaskUpdater: ProgressTaskUpdater? + ) { mDatabaseKDB?.let { throw IODatabaseException("Unable to merge from a database V1") @@ -706,7 +663,7 @@ class Database { try { val databaseUri = databaseToMerge.fileUri if (databaseUri != null) { - databaseToMerge.readDatabaseStream(contentResolver, databaseUri, + readDatabaseStream(contentResolver, databaseUri, { databaseInputStream -> val databaseToMergeKDB = DatabaseKDB() DatabaseInputKDB(databaseToMergeKDB) @@ -724,7 +681,7 @@ class Database { databaseToMergeKDB.masterKey = masterKey } } - databaseToMergeKDB + setDatabaseKDB(databaseToMergeKDB) }, { databaseInputStream -> val databaseToMergeKDBX = DatabaseKDBX() @@ -745,9 +702,10 @@ class Database { } } } - databaseToMergeKDBX + setDatabaseKDBX(databaseToMergeKDBX) } ) + loaded = true mDatabaseKDBX?.let { currentDatabaseKDBX -> val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply { @@ -769,8 +727,6 @@ class Database { } else { throw IODatabaseException("Database URI is null, database cannot be merged") } - } catch (e: FileNotFoundException) { - throw FileNotFoundDatabaseException("Unable to load the keyfile") } catch (e: LoadDatabaseException) { throw e } catch (e: Exception) { @@ -781,9 +737,11 @@ class Database { } @Throws(LoadDatabaseException::class) - fun reloadData(contentResolver: ContentResolver, - isRAMSufficient: (memoryWanted: Long) -> Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { + fun reloadData( + contentResolver: ContentResolver, + isRAMSufficient: (memoryWanted: Long) -> Boolean, + progressTaskUpdater: ProgressTaskUpdater? + ) { // Retrieve the stream from the old database URI try { @@ -799,7 +757,7 @@ class Database { .openDatabase(databaseInputStream, progressTaskUpdater) { databaseKDB.masterKey = masterKey } - databaseKDB + setDatabaseKDB(databaseKDB) }, { databaseInputStream -> val databaseKDBX = DatabaseKDBX() @@ -812,14 +770,13 @@ class Database { databaseKDBX.masterKey = masterKey } } - databaseKDBX + setDatabaseKDBX(databaseKDBX) } ) + loaded = true } else { throw IODatabaseException("Database URI is null, database cannot be reloaded") } - } catch (e: FileNotFoundException) { - throw FileNotFoundDatabaseException("Unable to load the keyfile") } catch (e: LoadDatabaseException) { throw e } catch (e: Exception) { @@ -851,13 +808,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 @@ -1367,5 +1324,73 @@ class Database { companion object : SingletonHolder(::Database) { private val TAG = Database::class.java.name + + @Throws(Exception::class) + private fun readDatabaseStream(contentResolver: ContentResolver, + databaseUri: Uri, + openDatabaseKDB: (InputStream) -> Unit, + openDatabaseKDBX: (InputStream) -> Unit) { + // Load Data, pass Uris as InputStreams + val databaseStream = UriUtil.getUriInputStream(contentResolver, databaseUri) + ?: throw IOException("Database input stream cannot be retrieve") + + BufferedInputStream(databaseStream).use { databaseInputStream -> + 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) -> openDatabaseKDB( + databaseInputStream + ) + // Header of database KDBX + DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> openDatabaseKDBX( + databaseInputStream + ) + // Header not recognized + else -> throw SignatureDatabaseException() + } + } + } + + @Throws(LoadDatabaseException::class) + fun getTransformSeed( + contentResolver: ContentResolver, + databaseUri: Uri, + onTransformSeedRetrieved: (ByteArray?) -> Unit + ) { + try { + // Read database stream for the first time + readDatabaseStream( + contentResolver, databaseUri, + { _ -> + }, + { databaseKDBXInputStream -> + val header = DatabaseHeaderKDBX(DatabaseKDBX()) + header.loadFromFile(databaseKDBXInputStream) + val transformSeed = ByteArray(64) + header.transformSeed?.copyInto(transformSeed, 0, 0, 32) + // seed: 32 byte transform seed, needs to be padded before sent to the hardware + transformSeed.fill(32, 32, 64) + onTransformSeedRetrieved(transformSeed) + } + ) + } catch (e: LoadDatabaseException) { + throw e + } catch (e: Exception) { + throw LoadDatabaseException(e) + } + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt index f483857c1..a8814454b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt @@ -1,13 +1,11 @@ package com.kunzisoft.keepass.hardware enum class HardwareKey(val value: String) { - HMAC_SHA1_KPXC("HMAC-SHA1 KPXC"), - HMAC_SHA1_KP2("HMAC-SHA1 KP2"), - OATH_HOTP("OATH HOTP"), - HMAC_SECRET_FIDO2("HMAC-SECRET FIDO2"); + FIDO2_SECRET("FIDO2 secret"), + CHALLENGE_RESPONSE_YUBIKEY("Yubikey challenge-response"); companion object { - val DEFAULT = HMAC_SHA1_KPXC + val DEFAULT = FIDO2_SECRET fun getStringValues(): List { return values().map { it.value } @@ -15,10 +13,8 @@ enum class HardwareKey(val value: String) { fun fromPosition(position: Int): HardwareKey { return when (position) { - 0 -> HMAC_SHA1_KPXC - 1 -> HMAC_SHA1_KP2 - 2 -> OATH_HOTP - 3 -> HMAC_SECRET_FIDO2 + 0 -> FIDO2_SECRET + 1 -> CHALLENGE_RESPONSE_YUBIKEY else -> DEFAULT } } diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt index a0efa42e1..6551999be 100644 --- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt @@ -1,10 +1,9 @@ package com.kunzisoft.keepass.hardware import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.ContentResolver import android.content.Intent import android.net.Uri +import android.os.Bundle import android.util.Log import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultCallback @@ -12,12 +11,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import com.kunzisoft.keepass.database.element.database.DatabaseKDBX -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX -import com.kunzisoft.keepass.utils.UriUtil -import java.io.BufferedInputStream -import java.io.IOException -import java.io.InputStream +import com.kunzisoft.keepass.database.element.Database class HardwareKeyResponseHelper { @@ -36,14 +30,16 @@ class HardwareKeyResponseHelper { this.fragment = context } - fun buildHardwareKeyResponse(onChallengeResponded: ((challengeResponse:ByteArray?) -> Unit)?) { + fun buildHardwareKeyResponse(onChallengeResponded: ((challengeResponse:ByteArray?, + extra: Bundle?) -> Unit)?) { val resultCallback = ActivityResultCallback { result -> Log.d(TAG, "resultCode from ykdroid: " + result.resultCode) if (result.resultCode == Activity.RESULT_OK) { val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response") Log.d(TAG, "Response: " + challengeResponse.contentToString()) challengeResponse?.let { - onChallengeResponded?.invoke(challengeResponse) + onChallengeResponded?.invoke(challengeResponse, + result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) } } } @@ -61,37 +57,20 @@ class HardwareKeyResponseHelper { } } - fun launchChallengeForResponse(databaseUri: Uri) { - fragment?.context?.contentResolver ?: activity?.contentResolver ?.let { contentResolver -> - getTransformSeedFromHeader(databaseUri, contentResolver)?.let { seed -> - try { + fun launchChallengeForResponse(databaseUri: Uri, extra: Bundle? = null) { + try { + fragment?.context?.contentResolver ?: activity?.contentResolver ?.let { contentResolver -> + Database.getTransformSeed(contentResolver, databaseUri) { seed -> getChallengeResponseResultLauncher?.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply { putExtra(YKDROID_SEED_KEY, seed) + putExtra(EXTRA_BUNDLE_KEY, extra) }) Log.d(TAG, "Challenge sent : " + seed.contentToString()) - } catch (e: ActivityNotFoundException) { - // TODO better error - throw IOException("No activity to handle $YKDROID_CHALLENGE_RESPONSE_INTENT intent") } } - } - } - - private fun getTransformSeedFromHeader(uri: Uri, contentResolver: ContentResolver): ByteArray? { - try { - BufferedInputStream(UriUtil.getUriInputStream(contentResolver, uri)).use { databaseInputStream -> - val header = DatabaseHeaderKDBX(DatabaseKDBX()) - header.loadFromFile(databaseInputStream) - val challenge = ByteArray(64) - header.transformSeed?.copyInto(challenge, 0, 0, 32) - // seed: 32 byte transform seed, needs to be padded before sent to the hardware - challenge.fill(32, 32, 64) - return challenge - } } catch (e: Exception) { - Log.e(TAG, "Could not read transform seed from file", e) + Log.e(TAG, "Could not launch challenge for response", e) } - return null } companion object { @@ -99,6 +78,7 @@ class HardwareKeyResponseHelper { private const val YKDROID_CHALLENGE_RESPONSE_INTENT = "net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" private const val YKDROID_SEED_KEY = "challenge" + private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY" } } \ No newline at end of file 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/res/layout/fragment_set_main_credential.xml b/app/src/main/res/layout/fragment_set_main_credential.xml index b17de8a52..5847882e5 100644 --- a/app/src/main/res/layout/fragment_set_main_credential.xml +++ b/app/src/main/res/layout/fragment_set_main_credential.xml @@ -131,11 +131,13 @@ + - Date: Mon, 9 May 2022 15:56:53 +0200 Subject: [PATCH 007/171] Open database with challenge response in service --- .../activities/MainCredentialActivity.kt | 39 ++----- .../SetMainCredentialDialogFragment.kt | 15 +-- .../activities/legacy/DatabaseActivity.kt | 6 +- .../AssignMainCredentialInDatabaseRunnable.kt | 10 +- .../database/action/CreateDatabaseRunnable.kt | 4 +- .../database/action/DatabaseTaskProvider.kt | 87 ++++++++++++--- .../database/action/LoadDatabaseRunnable.kt | 3 + .../database/action/MergeDatabaseRunnable.kt | 3 + .../keepass/database/element/Database.kt | 49 ++++++--- .../element/database/DatabaseVersioned.kt | 2 + .../database/exception/DatabaseException.kt | 1 + .../database/file/input/DatabaseInput.kt | 2 +- .../database/file/input/DatabaseInputKDB.kt | 5 +- .../database/file/input/DatabaseInputKDBX.kt | 7 +- .../hardware/HardwareKeyResponseHelper.kt | 52 ++++++--- .../kunzisoft/keepass/model/MainCredential.kt | 27 ++--- .../DatabaseTaskNotificationService.kt | 103 +++++++++++++++--- .../keepass/view/MainCredentialView.kt | 24 ++-- 18 files changed, 288 insertions(+), 151 deletions(-) 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 7254357ce..3d22a6bc4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -55,11 +55,11 @@ import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockManager +import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database 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.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK @@ -103,16 +103,20 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private var mRememberKeyFile: Boolean = false private var mExternalFileHelper: ExternalFileHelper? = null - private var mHardwareKeyResponseHelper: HardwareKeyResponseHelper? = null - private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false + private var mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) + private var mAutofillActivityResultLauncher: ActivityResultLauncher? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) AutofillHelper.buildActivityResultLauncher(this) else null + override fun initializeDatabaseTaskProvider(): DatabaseTaskProvider { + return DatabaseTaskProvider(this, mHardwareKeyResponseHelper) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -150,35 +154,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu loadDatabase() } - // Build elements to manage hardware key - mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) - mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData, _ -> - mainCredentialView?.validateCredential(responseData) - } - mainCredentialView?.onRequestHardwareKeyResponse = { hardwareKey -> - try { - when (hardwareKey) { - HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { - mDatabaseFileUri?.let { databaseUri -> - mHardwareKeyResponseHelper?.launchChallengeForResponse(databaseUri) - } - } - else -> { - // TODO other algorithm - } - } - } catch (e: Exception) { - Log.e(TAG, "Unable to retrieve the challenge response", e) - e.message?.let { message -> - Snackbar.make( - coordinatorLayout, - message, - Snackbar.LENGTH_LONG - ).asError().show() - } - } - } - // If is a view intent getUriFromIntent(intent) 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 21b9a7a9f..af76245fc 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 @@ -36,7 +36,6 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.hardware.HardwareKey -import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.utils.UriUtil @@ -50,7 +49,6 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private var mMasterPassword: String? = null private var mKeyFileUri: Uri? = null private var mHardwareKey: HardwareKey? = null - private var mChallengeResponse: ByteArray? = null private lateinit var rootView: View @@ -74,8 +72,6 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private var mNoKeyConfirmationDialog: AlertDialog? = null private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null - private var mHardwareKeyResponseHelper: HardwareKeyResponseHelper? = null - private var mAllowNoMasterKey: Boolean = false private val passwordTextWatcher = object : TextWatcher { @@ -180,7 +176,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { mMasterPassword = "" mKeyFileUri = null - mChallengeResponse = null + mHardwareKey = null if (verifyHardwareKey()) { approveMainCredential() @@ -219,16 +215,11 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } } - fun setChallengeResponse(response: ByteArray?) { - mChallengeResponse = response - approveMainCredential() - } - private fun retrieveMainCredential(): MainCredential { val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null - val hardwareKeyData = if (hardwareKeyCheckBox.isChecked) mChallengeResponse else null - return MainCredential(masterPassword, keyFileUri, hardwareKeyData) + val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null + return MainCredential(masterPassword, keyFileUri, hardwareKey) } override fun onResume() { 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..802967e3b 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 @@ -20,7 +20,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mDatabaseTaskProvider = DatabaseTaskProvider(this) + mDatabaseTaskProvider = initializeDatabaseTaskProvider() mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> val databaseWasReloaded = database?.wasReloaded == true @@ -36,6 +36,10 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { } } + open fun initializeDatabaseTaskProvider(): DatabaseTaskProvider { + return DatabaseTaskProvider(this) + } + override fun onDatabaseRetrieved(database: Database?) { mDatabase = database mDatabaseViewModel.defineDatabase(database) 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 f1be79ea1..228917779 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,14 +24,15 @@ 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.hardware.HardwareKey import com.kunzisoft.keepass.model.MainCredential -import com.kunzisoft.keepass.utils.UriUtil open class AssignMainCredentialInDatabaseRunnable ( context: Context, database: Database, protected val mDatabaseUri: Uri, - protected val mMainCredential: MainCredential) + protected val mMainCredential: MainCredential, + protected val mChallengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?) : SaveDatabaseRunnable(context, database, true) { private var mBackupKey: ByteArray? = null @@ -39,10 +40,11 @@ open class AssignMainCredentialInDatabaseRunnable ( override fun onStartRun() { // Set key try { + // TODO Move in assign master key mBackupKey = ByteArray(database.masterKey.size) - System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) + database.masterKey.copyInto(mBackupKey!!) - database.assignMasterKey(context.contentResolver, mMainCredential) + database.assignMasterKey(context.contentResolver, mMainCredential, mChallengeResponseRetriever) } 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..804247abd 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,6 +24,7 @@ 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.hardware.HardwareKey import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil @@ -34,8 +35,9 @@ class CreateDatabaseRunnable(context: Context, private val rootName: String, private val templateGroupName: String?, 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 { 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..235f6f5b2 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 @@ -42,6 +42,8 @@ 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.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo @@ -82,7 +84,7 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.util.* /** @@ -111,11 +113,36 @@ class DatabaseTaskProvider { private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null - constructor(activity: FragmentActivity) { + // To manage hardware key challenge response + private var mHardwareKeyResponseHelper: HardwareKeyResponseHelper? = null + + private var mChallengeResponseData: ByteArray? = null + private var mCancelChallengeResponse: Boolean = false + + constructor(activity: FragmentActivity, + hardwareKeyResponseHelper: HardwareKeyResponseHelper? = null) { this.activity = activity this.context = activity this.intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java) + + if (hardwareKeyResponseHelper != null) { + this.mHardwareKeyResponseHelper = hardwareKeyResponseHelper + this.mHardwareKeyResponseHelper?.buildHardwareKeyResponse { responseData, _ -> + // TODO Verify database + mChallengeResponseData = responseData ?: ByteArray(0) + respondToChallengeIfAllowed() + } + this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener { + override fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) { + mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed) + } + } + } else { + // It's not a credential screen, cancel the challenge response + mCancelChallengeResponse = true + cancelChallengeResponseIfAllowed() + } } constructor(service: Service) { @@ -181,6 +208,26 @@ class DatabaseTaskProvider { } } + private var requestChallengeListener: DatabaseTaskNotificationService.RequestChallengeListener? = null + + private fun respondToChallengeIfAllowed() { + // To wait binder and response + mBinder?.let { binder -> + mChallengeResponseData?.let { responseData -> + binder.getService().respondToChallenge(responseData) + mChallengeResponseData = null + } + } + } + + private fun cancelChallengeResponseIfAllowed() { + mBinder?.let { binder -> + if (mCancelChallengeResponse) { + binder.getService().cancelChallengeResponse() + } + } + } + private fun startDialog(titleId: Int? = null, messageId: Int? = null, warningId: Int? = null) { @@ -226,25 +273,41 @@ 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() } + respondToChallengeIfAllowed() + cancelChallengeResponseIfAllowed() } 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) + requestChallengeListener?.let { + service?.addRequestChallengeListener(it) + } + } + + private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) { + service?.removeActionTaskListener(actionTaskListener) + service?.removeDatabaseFileInfoListener(databaseInfoListener) + service?.removeDatabaseListener(databaseListener) + requestChallengeListener?.let { + service?.removeRequestChallengeListener(it) + } + } + private fun bindService() { initServiceConnection() serviceConnection?.let { @@ -262,10 +325,6 @@ class DatabaseTaskProvider { serviceConnection = null } - fun isBinded(): Boolean { - return mBinder != null - } - fun registerProgressTask() { stopDialog() @@ -299,9 +358,7 @@ class DatabaseTaskProvider { fun unregisterProgressTask() { stopDialog() - mBinder?.removeActionTaskListener(actionTaskListener) - mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) - mBinder?.removeDatabaseListener(databaseListener) + removeServiceListeners(mBinder) mBinder = null unBindService() 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 fe0546f86..6e735df36 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 @@ -26,6 +26,7 @@ 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.hardware.HardwareKey import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil @@ -37,6 +38,7 @@ class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, 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, @@ -55,6 +57,7 @@ class LoadDatabaseRunnable(private val context: Context, context.contentResolver, mDatabaseUri, mMainCredential, + mChallengeResponseRetriever, mReadonly, UriUtil.getBinaryDir(context), { memoryWanted -> 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 aad8519ca..6de21eac6 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 @@ -24,6 +24,7 @@ import android.net.Uri 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.hardware.HardwareKey import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable @@ -33,6 +34,7 @@ class MergeDatabaseRunnable(private val context: Context, private val mDatabase: Database, private val mDatabaseToMergeUri: Uri?, private val mDatabaseToMergeMainCredential: MainCredential?, + private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, private val progressTaskUpdater: ProgressTaskUpdater?, private val mLoadDatabaseResult: ((Result) -> Unit)?) : ActionRunnable() { @@ -47,6 +49,7 @@ class MergeDatabaseRunnable(private val context: Context, context.contentResolver, mDatabaseToMergeUri, mDatabaseToMergeMainCredential, + mDatabaseToMergeChallengeResponseRetriever, { memoryWanted -> BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) }, 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 d3768c47e..6bd8fde32 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,6 +54,7 @@ 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 @@ -388,6 +389,9 @@ class Database { dataModifiedSinceLastLoading = true } + val transformSeed: ByteArray? + get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed + var rootGroup: Group? get() { mDatabaseKDB?.rootGroup?.let { @@ -577,6 +581,7 @@ class Database { contentResolver: ContentResolver, databaseUri: Uri, mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, readOnly: Boolean, cacheDirectory: File, isRAMSufficient: (memoryWanted: Long) -> Boolean, @@ -601,11 +606,11 @@ class Database { DatabaseInputKDB(databaseKDB) .openDatabase(databaseInputStream, progressTaskUpdater - ) { + ) { seed -> databaseKDB.retrieveMasterKey( mainCredential.masterPassword, getKeyFileData(contentResolver, mainCredential.keyFileUri), - mainCredential.hardwareKeyData + challengeResponseRetriever.invoke(mainCredential.hardwareKey, seed) ) } setDatabaseKDB(databaseKDB) @@ -618,11 +623,11 @@ class Database { DatabaseInputKDBX(databaseKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, - progressTaskUpdater) { + progressTaskUpdater) { seed -> databaseKDBX.retrieveMasterKey( mainCredential.masterPassword, getKeyFileData(contentResolver, mainCredential.keyFileUri), - mainCredential.hardwareKeyData + challengeResponseRetriever.invoke(mainCredential.hardwareKey, seed) ) } } @@ -648,6 +653,7 @@ class Database { contentResolver: ContentResolver, databaseToMergeUri: Uri?, databaseToMergeMainCredential: MainCredential?, + databaseToMergeChallengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, isRAMSufficient: (memoryWanted: Long) -> Boolean, progressTaskUpdater: ProgressTaskUpdater? ) { @@ -667,7 +673,7 @@ class Database { { databaseInputStream -> val databaseToMergeKDB = DatabaseKDB() DatabaseInputKDB(databaseToMergeKDB) - .openDatabase(databaseInputStream, progressTaskUpdater) { + .openDatabase(databaseInputStream, progressTaskUpdater) { seed -> if (databaseToMergeMainCredential != null) { databaseToMergeKDB.retrieveMasterKey( databaseToMergeMainCredential.masterPassword, @@ -675,10 +681,12 @@ class Database { contentResolver, databaseToMergeMainCredential.keyFileUri, ), - databaseToMergeMainCredential.hardwareKeyData + databaseToMergeChallengeResponseRetriever + .invoke(databaseToMergeMainCredential.hardwareKey, seed) ) } else { databaseToMergeKDB.masterKey = masterKey + databaseToMergeKDB.transformSeed = transformSeed } } setDatabaseKDB(databaseToMergeKDB) @@ -687,7 +695,7 @@ class Database { val databaseToMergeKDBX = DatabaseKDBX() DatabaseInputKDBX(databaseToMergeKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) - openDatabase(databaseInputStream, progressTaskUpdater) { + openDatabase(databaseInputStream, progressTaskUpdater) { seed -> if (databaseToMergeMainCredential != null) { databaseToMergeKDBX.retrieveMasterKey( databaseToMergeMainCredential.masterPassword, @@ -695,10 +703,12 @@ class Database { contentResolver, databaseToMergeMainCredential.keyFileUri ), - databaseToMergeMainCredential.hardwareKeyData + databaseToMergeChallengeResponseRetriever + .invoke(databaseToMergeMainCredential.hardwareKey, seed) ) } else { databaseToMergeKDBX.masterKey = masterKey + databaseToMergeKDBX.transformSeed = transformSeed } } } @@ -756,6 +766,7 @@ class Database { DatabaseInputKDB(databaseKDB) .openDatabase(databaseInputStream, progressTaskUpdater) { databaseKDB.masterKey = masterKey + databaseKDB.transformSeed = transformSeed } setDatabaseKDB(databaseKDB) }, @@ -768,6 +779,7 @@ class Database { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { databaseKDBX.masterKey = masterKey + databaseKDBX.transformSeed = transformSeed } } setDatabaseKDBX(databaseKDBX) @@ -880,14 +892,14 @@ class Database { try { outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri) outputStream?.let { definedOutputStream -> + // TODO seed val databaseOutput = - mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) } - ?: mDatabaseKDBX?.let { - DatabaseOutputKDBX( - it, - definedOutputStream - ) - } + mDatabaseKDB?.let { + DatabaseOutputKDB(it, definedOutputStream) + } + ?: mDatabaseKDBX?.let { + DatabaseOutputKDBX(it, definedOutputStream) + } databaseOutput?.output() } } catch (e: Exception) { @@ -959,17 +971,18 @@ class Database { @Throws(IOException::class) fun assignMasterKey(contentResolver: ContentResolver, - mainCredential: MainCredential) { + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?) { val keyFileData = getKeyFileData(contentResolver, mainCredential.keyFileUri) mDatabaseKDB?.retrieveMasterKey( mainCredential.masterPassword, keyFileData, - mainCredential.hardwareKeyData + challengeResponseRetriever.invoke(mainCredential.hardwareKey, mDatabaseKDB?.transformSeed) ) mDatabaseKDBX?.retrieveMasterKey( mainCredential.masterPassword, keyFileData, - mainCredential.hardwareKeyData + challengeResponseRetriever.invoke(mainCredential.hardwareKey, mDatabaseKDBX?.transformSeed) ) mDatabaseKDBX?.keyLastChanged = DateInstant() } 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 cded530c7..f88852d92 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 @@ -61,6 +61,8 @@ abstract class DatabaseVersioned< var finalKey: ByteArray? = null protected set + var transformSeed: ByteArray? = null + abstract val version: String abstract val defaultFileExtension: String 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..f57d3a67c 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 @@ -107,6 +107,7 @@ class InvalidCredentialsDatabaseException : LoadDatabaseException { @StringRes override var errorId: Int = R.string.invalid_credentials constructor() : super() + constructor(string: String) : super(string) constructor(exception: Throwable) : super(exception) } 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..20d13eec9 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 @@ -44,7 +44,7 @@ abstract class DatabaseInput> (protected var m @Throws(LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: (() -> Unit)): D + assignMasterKey: ((seed: ByteArray?) -> Unit)): D protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) 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..556987430 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 @@ -50,7 +50,7 @@ class DatabaseInputKDB(database: DatabaseKDB) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: (() -> Unit)): DatabaseKDB { + assignMasterKey: ((seed: ByteArray?) -> Unit)): DatabaseKDB { try { startKeyTimer(progressTaskUpdater) @@ -76,7 +76,8 @@ class DatabaseInputKDB(database: DatabaseKDB) throw VersionDatabaseException() } - assignMasterKey.invoke() + mDatabase.transformSeed = header.transformSeed + assignMasterKey.invoke(header.transformSeed) // Select algorithm when { 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..56aa5daf5 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 @@ -46,6 +46,7 @@ import com.kunzisoft.keepass.stream.HashedBlockInputStream import com.kunzisoft.keepass.stream.HmacBlockInputStream import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.* +import kotlinx.coroutines.yield import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory @@ -102,7 +103,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: (() -> Unit)): DatabaseKDBX { + assignMasterKey: ((seed: ByteArray?) -> Unit)): DatabaseKDBX { try { startKeyTimer(progressTaskUpdater) @@ -114,7 +115,9 @@ class DatabaseInputKDBX(database: DatabaseKDBX) hashOfHeader = headerAndHash.hash val pbHeader = headerAndHash.header - assignMasterKey.invoke() + val transformSeed = header.transformSeed + mDatabase.transformSeed = transformSeed + assignMasterKey.invoke(transformSeed) mDatabase.makeFinalKey(header.masterSeed) stopKeyTimer() diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt index 6551999be..2aff8bdde 100644 --- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt @@ -2,16 +2,15 @@ package com.kunzisoft.keepass.hardware import android.app.Activity import android.content.Intent -import android.net.Uri import android.os.Bundle import android.util.Log +import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import com.kunzisoft.keepass.database.element.Database class HardwareKeyResponseHelper { @@ -30,17 +29,19 @@ class HardwareKeyResponseHelper { this.fragment = context } - fun buildHardwareKeyResponse(onChallengeResponded: ((challengeResponse:ByteArray?, - extra: Bundle?) -> Unit)?) { + fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?, + extra: Bundle?) -> Unit) { val resultCallback = ActivityResultCallback { result -> Log.d(TAG, "resultCode from ykdroid: " + result.resultCode) if (result.resultCode == Activity.RESULT_OK) { val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response") Log.d(TAG, "Response: " + challengeResponse.contentToString()) - challengeResponse?.let { - onChallengeResponded?.invoke(challengeResponse, - result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) - } + onChallengeResponded.invoke(challengeResponse, + result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) + } else { + Log.e(TAG, "Response error") + onChallengeResponded.invoke(null, + result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) } } @@ -57,19 +58,40 @@ class HardwareKeyResponseHelper { } } - fun launchChallengeForResponse(databaseUri: Uri, extra: Bundle? = null) { + fun launchChallengeForResponse(hardwareKey: HardwareKey?, seed: ByteArray?) { try { - fragment?.context?.contentResolver ?: activity?.contentResolver ?.let { contentResolver -> - Database.getTransformSeed(contentResolver, databaseUri) { seed -> + when (hardwareKey) { + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { + // 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 getChallengeResponseResultLauncher?.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply { - putExtra(YKDROID_SEED_KEY, seed) - putExtra(EXTRA_BUNDLE_KEY, extra) + putExtra(YKDROID_SEED_KEY, challenge) }) - Log.d(TAG, "Challenge sent : " + seed.contentToString()) + Log.d(TAG, "Challenge sent : " + challenge.contentToString()) + } + else -> { + // TODO other algorithm } } } catch (e: Exception) { - Log.e(TAG, "Could not launch challenge for response", e) + Log.e( + TAG, + "Unable to retrieve the challenge response", + e + ) + e.message?.let { message -> + Toast.makeText( + activity, + message, + Toast.LENGTH_LONG + ).show() + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt index fc2f31eee..88e00bc2b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt @@ -21,32 +21,24 @@ package com.kunzisoft.keepass.model import android.net.Uri import android.os.Parcel import android.os.Parcelable +import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.utils.readEnum +import com.kunzisoft.keepass.utils.writeEnum data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null, - var hardwareKeyData: ByteArray? = null): Parcelable { + var hardwareKey: HardwareKey? = null): Parcelable { constructor(parcel: Parcel) : this() { masterPassword = parcel.readString() keyFileUri = parcel.readParcelable(Uri::class.java.classLoader) - val hardwareKeyDataLength = parcel.readInt() - if (hardwareKeyDataLength >= 0) { - hardwareKeyData = ByteArray(hardwareKeyDataLength) - parcel.readByteArray(hardwareKeyData!!) - } else { - hardwareKeyData = null - } + hardwareKey = parcel.readEnum() } override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(masterPassword) parcel.writeParcelable(keyFileUri, flags) - if (hardwareKeyData != null) { - parcel.writeInt(hardwareKeyData!!.size) - parcel.writeByteArray(hardwareKeyData) - } else { - parcel.writeInt(-1) - } + parcel.writeEnum(hardwareKey) } override fun describeContents(): Int { @@ -61,10 +53,7 @@ data class MainCredential(var masterPassword: String? = null, if (masterPassword != other.masterPassword) return false if (keyFileUri != other.keyFileUri) return false - if (hardwareKeyData != null) { - if (other.hardwareKeyData == null) return false - if (!hardwareKeyData.contentEquals(other.hardwareKeyData)) return false - } else if (other.hardwareKeyData != null) return false + if (hardwareKey != other.hardwareKey) return false return true } @@ -72,7 +61,7 @@ data class MainCredential(var masterPassword: String? = null, override fun hashCode(): Int { var result = masterPassword?.hashCode() ?: 0 result = 31 * result + (keyFileUri?.hashCode() ?: 0) - result = 31 * result + (hardwareKeyData?.contentHashCode() ?: 0) + result = 31 * result + (hardwareKey?.hashCode() ?: 0) return result } 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..eb17be983 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -41,6 +41,8 @@ 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.database.exception.InvalidCredentialsDatabaseException +import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo @@ -53,6 +55,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 { @@ -63,10 +66,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress 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 listener or a response + private var mRequestChallengeListenerChannel = Channel(0) + private var mResponseChallengeChannel = Channel(0) + private var mActionRunning = false private var mTaskRemovedRequested = false private var mCreationState = false @@ -76,6 +83,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private var mMessageId: Int? = null private var mWarningId: Int? = null + override fun retrieveChannelId(): String { return CHANNEL_DATABASE_ID } @@ -114,6 +122,23 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun removeActionTaskListener(actionTaskListener: ActionTaskListener) { mActionTaskListeners.remove(actionTaskListener) } + + fun addRequestChallengeListener(requestChallengeListener: RequestChallengeListener) { + mainScope.launch { + if (!mRequestChallengeListenerChannel.isEmpty) { + mRequestChallengeListenerChannel.cancel(CancellationException("Challenge already requested")) + mRequestChallengeListenerChannel = Channel(0) + } else { + mRequestChallengeListenerChannel.send(requestChallengeListener) + } + } + } + + fun removeRequestChallengeListener(requestChallengeListener: RequestChallengeListener) { + mainScope.launch { + //mRequestChallengeListenerChannel.cancel() + } + } } interface DatabaseListener { @@ -131,6 +156,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) } + interface RequestChallengeListener { + fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) + } + fun checkDatabase() { mDatabaseListeners.forEach { databaseListener -> databaseListener.onDatabaseRetrieved(mDatabase) @@ -203,6 +232,26 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } + fun respondToChallenge(response: ByteArray) { + mainScope.launch { + if (response.isEmpty()) { + mResponseChallengeChannel.cancel(CancellationException(("Unable to get the response from challenge"))) + mResponseChallengeChannel = Channel(0) + } else { + mResponseChallengeChannel.send(response) + } + } + } + + fun cancelChallengeResponse() { + mainScope.launch { + mRequestChallengeListenerChannel.cancel() + mRequestChallengeListenerChannel = Channel(0) + mResponseChallengeChannel.cancel() + mResponseChallengeChannel = Channel(0) + } + } + override fun onBind(intent: Intent): IBinder? { super.onBind(intent) return mActionTaskBinder @@ -539,6 +588,22 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress super.onTaskRemoved(rootIntent) } + private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey?, + seed: ByteArray?): ByteArray? { + if (hardwareKey == null || seed == null) + return null + // Request a challenge - response + var response: ByteArray? + runBlocking { + // Send the request + val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive() + challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed) + // Wait the response + response = mResponseChallengeChannel.receive() + } + return response + } + private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) @@ -558,7 +623,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress 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) @@ -589,14 +657,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress 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 { @@ -626,6 +697,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress database, databaseToMergeUri, databaseToMergeMainCredential, + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + }, this ) { result -> // No need to add each info to reload database @@ -652,7 +726,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress AssignMainCredentialInDatabaseRunnable(this, database, databaseUri, - intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() + intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential(), + { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } ) } else { null 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 ea75c4504..acee02799 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -30,12 +30,14 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.widget.* +import android.widget.CompoundButton +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener -import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.model.MainCredential @@ -55,9 +57,6 @@ class MainCredentialView @JvmOverloads constructor(context: Context, var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null var onValidateListener: (() -> Unit)? = null - var onRequestHardwareKeyResponse: ((HardwareKey)-> Unit)? = null - - private var mChallengeResponse: ByteArray? = null private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD @@ -119,15 +118,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context, } } - fun validateCredential(hardwareKeyData: ByteArray? = null) { - val hardwareKey = hardwareKeySelectionView.hardwareKey - if (hardwareKeyData == null && checkboxHardwareView.isChecked) { - onRequestHardwareKeyResponse?.invoke(hardwareKey) - } else { - mChallengeResponse = hardwareKeyData - onValidateListener?.invoke() - mChallengeResponse = null - } + fun validateCredential() { + onValidateListener?.invoke() } fun populatePasswordTextView(text: String?) { @@ -170,8 +162,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context, passwordTextView.text?.toString() else null this.keyFileUri = if (checkboxKeyFileView.isChecked) keyFileSelectionView.uri else null - this.hardwareKeyData = if (checkboxHardwareView.isChecked) - mChallengeResponse else null + this.hardwareKey = if (checkboxHardwareView.isChecked) + hardwareKeySelectionView.hardwareKey else null } } From 8b2f99476995ae8cbd05720caf4d21e1773ae4af Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 10 May 2022 15:02:22 +0200 Subject: [PATCH 008/171] Save database with challenge response --- .../activities/MainCredentialActivity.kt | 2 +- .../SetMainCredentialDialogFragment.kt | 29 +- .../AssignMainCredentialInDatabaseRunnable.kt | 9 +- .../database/action/CreateDatabaseRunnable.kt | 6 +- .../database/action/LoadDatabaseRunnable.kt | 2 +- .../database/action/MergeDatabaseRunnable.kt | 2 +- .../RemoveUnlinkedDataDatabaseRunnable.kt | 6 +- .../database/action/SaveDatabaseRunnable.kt | 10 +- ...dateCompressionBinariesDatabaseRunnable.kt | 6 +- .../DeleteEntryHistoryDatabaseRunnable.kt | 6 +- .../RestoreEntryHistoryDatabaseRunnable.kt | 19 +- .../action/node/ActionNodeDatabaseRunnable.kt | 6 +- .../database/action/node/AddEntryRunnable.kt | 6 +- .../database/action/node/AddGroupRunnable.kt | 6 +- .../database/action/node/CopyNodesRunnable.kt | 10 +- .../action/node/DeleteNodesRunnable.kt | 10 +- .../database/action/node/MoveNodesRunnable.kt | 10 +- .../action/node/UpdateEntryRunnable.kt | 6 +- .../action/node/UpdateGroupRunnable.kt | 6 +- .../keepass/database/element/CompositeKey.kt | 237 ++++++++++++++ .../keepass/database/element/Database.kt | 293 ++++++++++++------ .../database/element/database/DatabaseKDB.kt | 23 +- .../database/element/database/DatabaseKDBX.kt | 141 +-------- .../element/database/DatabaseVersioned.kt | 80 +---- .../database/file/output/DatabaseOutputKDB.kt | 1 - .../file/output/DatabaseOutputKDBX.kt | 6 +- .../kunzisoft/keepass/model/MainCredential.kt | 10 +- .../DatabaseTaskNotificationService.kt | 91 ++++-- .../keepass/settings/SettingsActivity.kt | 8 + .../keepass/view/HardwareKeySelectionView.kt | 13 +- .../keepass/view/MainCredentialView.kt | 2 +- .../layout/fragment_set_main_credential.xml | 6 +- 32 files changed, 642 insertions(+), 426 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt 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 3d22a6bc4..bdf85beb0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -427,7 +427,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 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 af76245fc..f9f6c9a4b 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 @@ -178,9 +178,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { mKeyFileUri = null mHardwareKey = null - if (verifyHardwareKey()) { - approveMainCredential() - } + approveMainCredential() } val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) negativeButton.setOnClickListener { @@ -196,7 +194,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } private fun approveMainCredential() { - var error = verifyPassword() || verifyKeyFile() + var error = verifyPassword() || verifyKeyFile() || verifyHardwareKey() if (!passwordCheckBox.isChecked && !keyFileCheckBox.isChecked && !hardwareKeyCheckBox.isChecked @@ -250,7 +248,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { if ((mMasterPassword.isNullOrEmpty()) && (!keyFileCheckBox.isChecked - || keyFileSelectionView.uri == null)) { + || keyFileSelectionView.uri == null) + && (!hardwareKeyCheckBox.isChecked + || hardwareKeySelectionView.hardwareKey == null)) { error = true showEmptyPasswordConfirmationDialog() } @@ -272,22 +272,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } private fun verifyHardwareKey(): Boolean { + var error = false if (hardwareKeyCheckBox.isChecked) { - try { - when (hardwareKeySelectionView.hardwareKey) { - HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { - // TODO Yubikey algorithm - } - else -> { - // TODO other algorithm - } - } - } catch (e: Exception) { - // TODO Log.e(TAG, "Unable to retrieve the challenge response", e) + hardwareKeySelectionView.hardwareKey.let { hardwareKey -> + mHardwareKey = hardwareKey } - return false + // TODO verify drivers + // error = true } - return true + return error } private fun showEmptyPasswordConfirmationDialog() { 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 228917779..06478eef3 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 @@ -31,20 +31,17 @@ open class AssignMainCredentialInDatabaseRunnable ( context: Context, database: Database, protected val mDatabaseUri: Uri, - protected val mMainCredential: MainCredential, - protected val mChallengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?) - : SaveDatabaseRunnable(context, database, true) { + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + : SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) { private var mBackupKey: ByteArray? = null override fun onStartRun() { // Set key try { - // TODO Move in assign master key mBackupKey = ByteArray(database.masterKey.size) database.masterKey.copyInto(mBackupKey!!) - - database.assignMasterKey(context.contentResolver, mMainCredential, mChallengeResponseRetriever) } 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 804247abd..52a36bd98 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 @@ -34,8 +34,8 @@ class CreateDatabaseRunnable(context: Context, private val databaseName: String, private val rootName: String, private val templateGroupName: String?, - mainCredential: MainCredential, - challengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, + val mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, private val createDatabaseResult: ((Result) -> Unit)?) : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) { @@ -61,7 +61,7 @@ class CreateDatabaseRunnable(context: Context, if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context.applicationContext) .addOrUpdateDatabaseUri(mDatabaseUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) + if (PreferencesUtil.rememberKeyFileLocations(context)) mainCredential.keyFileUri else null) } // Register the current time to init the lock timer 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 6e735df36..0ce8d3d5b 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 @@ -38,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, private val mDatabaseUri: Uri, private val mMainCredential: MainCredential, - private val mChallengeResponseRetriever: (hardwareKey: HardwareKey?, seed: ByteArray?) -> ByteArray?, + private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray, private val mReadonly: Boolean, private val mCipherEncryptDatabase: CipherEncryptDatabase?, private val mFixDuplicateUUID: Boolean, 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 6de21eac6..a3c925d8e 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 @@ -34,7 +34,7 @@ class MergeDatabaseRunnable(private val context: Context, private val mDatabase: Database, private val mDatabaseToMergeUri: Uri?, private val mDatabaseToMergeMainCredential: MainCredential?, - private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, + private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, private val progressTaskUpdater: ProgressTaskUpdater?, private val mLoadDatabaseResult: ((Result) -> Unit)?) : ActionRunnable() { 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..027d4c319 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.model.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,11 @@ open class SaveDatabaseRunnable(protected var context: Context, database.checkVersion() if (saveDatabase && result.isSuccess) { try { - database.saveData(databaseCopyUri, context.contentResolver) + database.saveData( + context.contentResolver, + 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..3a429c92c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt @@ -0,0 +1,237 @@ +package com.kunzisoft.keepass.database.element + +import android.content.ContentResolver +import android.net.Uri +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.utils.StringUtil.removeSpaceChars +import com.kunzisoft.keepass.utils.StringUtil.toHexString +import com.kunzisoft.keepass.utils.UriUtil +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 CompositeKey(var passwordData: ByteArray? = null, + var keyFileData: ByteArray? = null, + var hardwareKeyData: ByteArray? = 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 (hardwareKeyData != null) { + if (other.hardwareKeyData == null) return false + if (!hardwareKeyData.contentEquals(other.hardwareKeyData)) return false + } else if (other.hardwareKeyData != null) return false + + return true + } + + override fun hashCode(): Int { + var result = passwordData?.contentHashCode() ?: 0 + result = 31 * result + (keyFileData?.contentHashCode() ?: 0) + result = 31 * result + (hardwareKeyData?.contentHashCode() ?: 0) + return result + } + + companion object { + + private val TAG = CompositeKey::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" + } +} \ 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 6bd8fde32..12371d532 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 @@ -26,6 +26,7 @@ import android.graphics.Color import android.net.Uri import android.util.Log import com.kunzisoft.androidclearchroma.ChromaUtil +import com.kunzisoft.encrypt.HashManager import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine @@ -43,7 +44,10 @@ import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.TemplateEngine -import com.kunzisoft.keepass.database.exception.* +import com.kunzisoft.keepass.database.exception.DatabaseOutputException +import com.kunzisoft.keepass.database.exception.IODatabaseException +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.database.exception.SignatureDatabaseException import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 @@ -62,6 +66,7 @@ import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.readBytes4ToUInt import java.io.* +import java.nio.charset.Charset import java.util.* @@ -74,6 +79,10 @@ class Database { var fileUri: Uri? = null private set + // To resave the database with same credential when already loaded + private var mMainCredential = MainCredential() + private var mCompositeKey = CompositeKey() + private var mSearchHelper: SearchHelper = SearchHelper() var isReadOnly = false @@ -385,6 +394,7 @@ class Database { set(masterKey) { mDatabaseKDB?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey + mDatabaseKDBX?.keyLastChanged = DateInstant() mDatabaseKDBX?.settingsChanged = DateInstant() dataModifiedSinceLastLoading = true } @@ -561,27 +571,12 @@ class Database { this.dataModifiedSinceLastLoading = false } - @Throws(Exception::class) - private fun getKeyFileData(contentResolver: ContentResolver, keyFileUri: Uri?): ByteArray? { - try { - keyFileUri?.let { uri -> - UriUtil.getUriInputStream(contentResolver, uri)?.use { keyFileInputStream -> - return keyFileInputStream.readBytes() - } - } - } catch (e: Exception) { - Log.e(TAG, "Unable to load the keyfile.", e) - throw LoadDatabaseException("Unable to load the keyfile.") - } - return null - } - @Throws(LoadDatabaseException::class) fun loadData( contentResolver: ContentResolver, databaseUri: Uri, mainCredential: MainCredential, - challengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, readOnly: Boolean, cacheDirectory: File, isRAMSufficient: (memoryWanted: Long) -> Boolean, @@ -607,10 +602,11 @@ class Database { .openDatabase(databaseInputStream, progressTaskUpdater ) { seed -> - databaseKDB.retrieveMasterKey( - mainCredential.masterPassword, - getKeyFileData(contentResolver, mainCredential.keyFileUri), - challengeResponseRetriever.invoke(mainCredential.hardwareKey, seed) + databaseKDB.masterKey = deriveKDBMasterKey( + databaseKDB, + contentResolver, + mainCredential, + challengeResponseRetriever ) } setDatabaseKDB(databaseKDB) @@ -624,10 +620,11 @@ class Database { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { seed -> - databaseKDBX.retrieveMasterKey( - mainCredential.masterPassword, - getKeyFileData(contentResolver, mainCredential.keyFileUri), - challengeResponseRetriever.invoke(mainCredential.hardwareKey, seed) + databaseKDBX.masterKey = deriveKDBXMasterKey( + databaseKDBX, + contentResolver, + mainCredential, + challengeResponseRetriever ) } } @@ -653,7 +650,7 @@ class Database { contentResolver: ContentResolver, databaseToMergeUri: Uri?, databaseToMergeMainCredential: MainCredential?, - databaseToMergeChallengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?, + databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, isRAMSufficient: (memoryWanted: Long) -> Boolean, progressTaskUpdater: ProgressTaskUpdater? ) { @@ -675,14 +672,11 @@ class Database { DatabaseInputKDB(databaseToMergeKDB) .openDatabase(databaseInputStream, progressTaskUpdater) { seed -> if (databaseToMergeMainCredential != null) { - databaseToMergeKDB.retrieveMasterKey( - databaseToMergeMainCredential.masterPassword, - getKeyFileData( - contentResolver, - databaseToMergeMainCredential.keyFileUri, - ), + databaseToMergeKDB.masterKey = deriveKDBMasterKey( + databaseToMergeKDB, + contentResolver, + databaseToMergeMainCredential, databaseToMergeChallengeResponseRetriever - .invoke(databaseToMergeMainCredential.hardwareKey, seed) ) } else { databaseToMergeKDB.masterKey = masterKey @@ -696,15 +690,13 @@ class Database { DatabaseInputKDBX(databaseToMergeKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { seed -> + // TODO challenge response if (databaseToMergeMainCredential != null) { - databaseToMergeKDBX.retrieveMasterKey( - databaseToMergeMainCredential.masterPassword, - getKeyFileData( - contentResolver, - databaseToMergeMainCredential.keyFileUri - ), + databaseToMergeKDBX.masterKey = deriveKDBXMasterKey( + databaseToMergeKDBX, + contentResolver, + databaseToMergeMainCredential, databaseToMergeChallengeResponseRetriever - .invoke(databaseToMergeMainCredential.hardwareKey, seed) ) } else { databaseToMergeKDBX.masterKey = masterKey @@ -798,6 +790,171 @@ class Database { } } + @Throws(DatabaseOutputException::class) + fun saveData(contentResolver: ContentResolver, + databaseCopyUri: Uri?, + mainCredential: MainCredential?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) { + 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) { seed -> + var masterKeyDerived : ByteArray? = null + if (mainCredential != null) { + // If composite key is null, build byte array from MainCredential + mDatabaseKDB?.let { databaseKDB -> + masterKeyDerived = deriveKDBMasterKey( + databaseKDB, + contentResolver, + mainCredential, + challengeResponseRetriever + ) + } + mDatabaseKDBX?.let { databaseKDBX -> + masterKeyDerived = deriveKDBXMasterKey( + databaseKDBX, + contentResolver, + mainCredential, + challengeResponseRetriever + ) + } + } else { + // Reuse composite key parts + mDatabaseKDB?.let { databaseKDB -> + masterKeyDerived = databaseKDB.masterKey + } + mDatabaseKDBX?.let { databaseKDBX -> + masterKeyDerived = deriveKDBXCompositeKey( + mMainCredential, + mCompositeKey, + challengeResponseRetriever + ) + } + } + masterKeyDerived ?: + throw DatabaseOutputException("Unable to derive master key without database.") + } + } + 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) + } + } + + @Throws(DatabaseOutputException::class) + private fun deriveKDBMasterKey( + databaseKDB: DatabaseKDB, + contentResolver: ContentResolver, + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ): ByteArray { + if (mainCredential.password == null && mainCredential.keyFileUri == null) + throw IllegalArgumentException("Key cannot be empty.") + if (mainCredential.hardwareKey != null) + throw IllegalArgumentException("Hardware key is not supported.") + return compositeKeyToMasterKey(retrieveCompositeKey( + contentResolver, + mainCredential, + databaseKDB.passwordEncoding, + databaseKDB.allowXMLKeyFile, + databaseKDB.transformSeed, + challengeResponseRetriever + )) + } + + @Throws(DatabaseOutputException::class) + private fun deriveKDBXMasterKey( + databaseKDBX: DatabaseKDBX, + contentResolver: ContentResolver, + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ): ByteArray { + return compositeKeyToMasterKey(retrieveCompositeKey( + contentResolver, + mainCredential, + databaseKDBX.passwordEncoding, + databaseKDBX.allowXMLKeyFile, + databaseKDBX.transformSeed, + challengeResponseRetriever + )) + } + + @Throws(DatabaseOutputException::class) + private fun deriveKDBXCompositeKey( + mainCredential: MainCredential, + compositeKey: CompositeKey, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ): ByteArray { + if (compositeKey.hardwareKeyData == null) + return compositeKeyToMasterKey(compositeKey) + val hardwareKey = mainCredential.hardwareKey + val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( + challengeResponseRetriever.invoke(hardwareKey, transformSeed) + ) else null + return compositeKeyToMasterKey(compositeKey.apply { + this.hardwareKeyData = hardwareKeyBytes + }) + } + + @Throws(IOException::class) + private fun retrieveCompositeKey(contentResolver: ContentResolver, + mainCredential: MainCredential, + passwordEncoding: Charset, + allowXMLKeyFile: Boolean, + transformSeed: ByteArray?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, + ): CompositeKey { + // Save to rebuild master password with new seed later + mMainCredential = mainCredential + val password = mainCredential.password + val keyFileUri = mainCredential.keyFileUri + val hardwareKey = mainCredential.hardwareKey + val passwordBytes = if (password != null) CompositeKey.retrievePasswordKey( + password, + passwordEncoding + ) else null + val keyFileBytes = if (keyFileUri != null) CompositeKey.retrieveFileKey( + contentResolver, + keyFileUri, + allowXMLKeyFile + ) else null + val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( + challengeResponseRetriever.invoke(hardwareKey, transformSeed) + ) else null + val compositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKeyBytes) + // Save to rebuild master password with new seed later + mCompositeKey = compositeKey + return compositeKey + } + + private fun compositeKeyToMasterKey(compositeKey: CompositeKey): ByteArray { + return HashManager.hashSha256( + compositeKey.passwordData, + compositeKey.keyFileData, + compositeKey.hardwareKeyData + ) + } + fun groupIsInRecycleBin(group: Group): Boolean { val groupKDB = group.groupKDB val groupKDBX = group.groupKDBX @@ -883,40 +1040,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 -> - // TODO seed - 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() @@ -944,6 +1067,8 @@ class Database { fun clearAndClose(context: Context? = null) { clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) }) + this.mMainCredential = MainCredential() + this.mCompositeKey = CompositeKey() this.mDatabaseKDB = null this.mDatabaseKDBX = null this.fileUri = null @@ -962,31 +1087,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(contentResolver: ContentResolver, - mainCredential: MainCredential, - challengeResponseRetriever: (HardwareKey?, ByteArray?) -> ByteArray?) { - val keyFileData = getKeyFileData(contentResolver, mainCredential.keyFileUri) - mDatabaseKDB?.retrieveMasterKey( - mainCredential.masterPassword, - keyFileData, - challengeResponseRetriever.invoke(mainCredential.hardwareKey, mDatabaseKDB?.transformSeed) - ) - mDatabaseKDBX?.retrieveMasterKey( - mainCredential.masterPassword, - keyFileData, - challengeResponseRetriever.invoke(mainCredential.hardwareKey, mDatabaseKDBX?.transformSeed) - ) - mDatabaseKDBX?.keyLastChanged = DateInstant() - } - fun rootCanContainsEntry(): Boolean { return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false } 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 48a3a2df8..cfd547fc9 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 @@ -32,6 +32,7 @@ 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 java.io.IOException +import java.nio.charset.Charset import java.util.* class DatabaseKDB : DatabaseVersioned() { @@ -55,8 +56,11 @@ class DatabaseKDB : DatabaseVersioned() { KdfFactory.aesKdf ) - override val passwordEncoding: String - get() = "ISO-8859-1" + override val passwordEncoding: Charset + get() = Charsets.ISO_8859_1 + + override val allowXMLKeyFile: Boolean + get() = false override var numberKeyEncryptionRounds = 300L @@ -115,21 +119,6 @@ class DatabaseKDB : DatabaseVersioned() { return newId } - @Throws(IOException::class) - override fun deriveMasterKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKey: ByteArray?): ByteArray { - return if (passwordKey != null && keyFileData != null) { - retrieveCompositeKey(passwordKey, keyFileData, null) ?: byteArrayOf() - } else if (passwordKey != null) { // key.length() >= 0 - retrievePasswordKey(passwordKey) - } else if (keyFileData != null) { // key == null - retrieveFileKey(keyFileData) - } 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 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 b95cf599d..5607c6dca 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 @@ -52,22 +52,14 @@ import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible 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.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 @@ -364,8 +356,11 @@ class DatabaseKDBX : DatabaseVersioned { kdfEngine.setParallelism(kdfParameters!!, parallelism) } - override val passwordEncoding: String - get() = "UTF-8" + override val passwordEncoding: Charset + get() = Charsets.UTF_8 + + override val allowXMLKeyFile: Boolean + get() = true private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? { if (groupUUID == UUID_ZERO) @@ -528,14 +523,6 @@ class DatabaseKDBX : DatabaseVersioned { return mFieldReferenceEngine.compile(textReference, recursionLevel) } - @Throws(IOException::class) - public override fun deriveMasterKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKey: ByteArray?): ByteArray { - return retrieveCompositeKey(passwordKey, keyFileData, hardwareKey) - ?: HashManager.hashSha256(byteArrayOf()) - } - @Throws(IOException::class) fun makeFinalKey(masterSeed: ByteArray) { @@ -607,115 +594,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 { @@ -924,13 +802,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 f88852d92..9b6686378 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< @@ -55,7 +52,8 @@ abstract class DatabaseVersioned< abstract val kdfAvailableList: List abstract var numberKeyEncryptionRounds: Long - protected abstract val passwordEncoding: String + abstract val passwordEncoding: Charset + abstract val allowXMLKeyFile: Boolean var masterKey = ByteArray(32) var finalKey: ByteArray? = null @@ -93,74 +91,6 @@ abstract class DatabaseVersioned< return getGroupIndexes().filter { it != rootGroup } } - @Throws(IOException::class) - protected abstract fun deriveMasterKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKey: ByteArray?): ByteArray - - @Throws(IOException::class) - fun retrieveMasterKey(key: String?, - keyFileData: ByteArray?, - hardwareKeyData: ByteArray?) { - masterKey = deriveMasterKey(key, keyFileData, hardwareKeyData) - } - - @Throws(IOException::class) - protected fun retrieveCompositeKey(passwordKey: String?, - keyFileData: ByteArray?, - hardwareKeyData: ByteArray?): ByteArray? { - if (passwordKey == null && keyFileData == null && hardwareKeyData == null) - return null - val passwordBytes = if (passwordKey != null) retrievePasswordKey(passwordKey) else null - val keyFileBytes = if (keyFileData != null) retrieveFileKey(keyFileData) else null - val hardwareKeyBytes = if (hardwareKeyData != null) retrieveHardwareKey(hardwareKeyData) else null - return HashManager.hashSha256( - passwordBytes, - keyFileBytes, - hardwareKeyBytes - ) - } - - @Throws(IOException::class) - protected fun retrievePasswordKey(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 retrieveFileKey(keyData: ByteArray): ByteArray { - try { - // 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) - } - } - - @Throws(IOException::class) - protected fun retrieveHardwareKey(keyData: ByteArray): ByteArray { - return HashManager.hashSha256(keyData) - } - protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { return null } @@ -176,14 +106,14 @@ 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 } 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..da556654d 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 @@ -68,7 +68,6 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, sortNodesForOutput() val header = outputHeader(mOutputStream) - val finalKey = getFinalKey(header) val cipher: Cipher = try { 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 7228efeb5..7caf878da 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 @@ -57,7 +57,8 @@ import kotlin.experimental.or class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, - outputStream: OutputStream) + outputStream: OutputStream, + private val mAssignMasterKey: ((seed: ByteArray?) -> ByteArray)) : DatabaseOutput(outputStream) { private var randomStream: StreamCipher? = null @@ -327,6 +328,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, val header = DatabaseHeaderKDBX(mDatabaseKDBX) setIVs(header) + // TODO Check modification + mDatabaseKDBX.masterKey = mAssignMasterKey.invoke(header.transformSeed) + val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream) pho.output() diff --git a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt index 88e00bc2b..8a7a3c479 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt @@ -25,18 +25,18 @@ import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.utils.readEnum import com.kunzisoft.keepass.utils.writeEnum -data class MainCredential(var masterPassword: String? = null, +data class MainCredential(var password: String? = null, var keyFileUri: Uri? = null, var hardwareKey: HardwareKey? = null): Parcelable { constructor(parcel: Parcel) : this() { - masterPassword = parcel.readString() + password = parcel.readString() keyFileUri = parcel.readParcelable(Uri::class.java.classLoader) hardwareKey = parcel.readEnum() } override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(masterPassword) + parcel.writeString(password) parcel.writeParcelable(keyFileUri, flags) parcel.writeEnum(hardwareKey) } @@ -51,7 +51,7 @@ data class MainCredential(var masterPassword: String? = null, other as MainCredential - if (masterPassword != other.masterPassword) return false + if (password != other.password) return false if (keyFileUri != other.keyFileUri) return false if (hardwareKey != other.hardwareKey) return false @@ -59,7 +59,7 @@ data class MainCredential(var masterPassword: String? = null, } override fun hashCode(): Int { - var result = masterPassword?.hashCode() ?: 0 + var result = password?.hashCode() ?: 0 result = 31 * result + (keyFileUri?.hashCode() ?: 0) result = 31 * result + (hardwareKey?.hashCode() ?: 0) return result 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 eb17be983..4da40c9bd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -41,7 +41,6 @@ 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.database.exception.InvalidCredentialsDatabaseException import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.MainCredential @@ -136,7 +135,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun removeRequestChallengeListener(requestChallengeListener: RequestChallengeListener) { mainScope.launch { - //mRequestChallengeListenerChannel.cancel() + // TODO mRequestChallengeListenerChannel.cancel() } } } @@ -429,7 +428,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_MERGE_TASK, ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database - ACTION_DATABASE_SAVE -> R.string.saving_database + ACTION_DATABASE_ASSIGN_PASSWORD_TASK, + ACTION_DATABASE_SAVE -> { + saveAction = true + R.string.saving_database + } else -> { R.string.command_execution } @@ -588,18 +591,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress super.onTaskRemoved(rootIntent) } - private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey?, - seed: ByteArray?): ByteArray? { - if (hardwareKey == null || seed == null) - return null + private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey, + seed: ByteArray?): ByteArray { // Request a challenge - response - var response: ByteArray? + var response: ByteArray runBlocking { // Send the request val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive() challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed) // Wait the response - response = mResponseChallengeChannel.receive() + response = mResponseChallengeChannel.receive() ?: byteArrayOf() } return response } @@ -726,11 +727,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress AssignMainCredentialInDatabaseRunnable(this, database, databaseUri, - intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential(), - { hardwareKey, seed -> - retrieveResponseFromChallenge(hardwareKey, seed) - } - ) + intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() + ) { hardwareKey, seed -> + retrieveResponseFromChallenge(hardwareKey, seed) + } } else { null } @@ -764,7 +764,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 @@ -789,7 +792,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 @@ -814,7 +820,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 @@ -839,7 +848,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 @@ -860,7 +872,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 @@ -881,7 +896,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 @@ -897,7 +915,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 } @@ -915,7 +936,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 @@ -934,7 +958,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 @@ -958,7 +985,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 } @@ -974,7 +1003,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 } @@ -988,7 +1019,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 @@ -1013,6 +1048,10 @@ 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 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..932dbce0f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -35,7 +35,9 @@ import com.kunzisoft.keepass.R 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.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -55,6 +57,12 @@ open class SettingsActivity private var toolbar: Toolbar? = null private var lockView: FloatingActionButton? = null + private var mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) + + override fun initializeDatabaseTaskProvider(): DatabaseTaskProvider { + return DatabaseTaskProvider(this, mHardwareKeyResponseHelper) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt index 88cfddf0b..32fc7880d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt @@ -23,10 +23,10 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, defStyle: Int = 0) : ConstraintLayout(context, attrs, defStyle) { - private var mHardwareKey: HardwareKey = HardwareKey.DEFAULT + private var mHardwareKey: HardwareKey? = null private val hardwareKeyCompletion: AppCompatAutoCompleteTextView - var selectionListener: ((HardwareKey)-> Unit)? = null + var selectionListener: ((HardwareKey?)-> Unit)? = null private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context) @@ -78,13 +78,14 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, } } - var hardwareKey: HardwareKey + var hardwareKey: HardwareKey? get() { return mHardwareKey } set(value) { mHardwareKey = value - hardwareKeyCompletion.setSelection(value.ordinal) + if (value != null) + hardwareKeyCompletion.setSelection(value.ordinal) } override fun onSaveInstanceState(): Parcelable { @@ -104,12 +105,12 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, } internal class SavedState : BaseSavedState { - var mHardwareKey: HardwareKey = HardwareKey.DEFAULT + var mHardwareKey: HardwareKey? = null constructor(superState: Parcelable?) : super(superState) private constructor(parcel: Parcel) : super(parcel) { - mHardwareKey = parcel.readEnum() ?: HardwareKey.DEFAULT + mHardwareKey = parcel.readEnum() } override fun writeToParcel(out: Parcel, flags: Int) { 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 acee02799..584f89428 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -158,7 +158,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context, 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 diff --git a/app/src/main/res/layout/fragment_set_main_credential.xml b/app/src/main/res/layout/fragment_set_main_credential.xml index 5847882e5..ec8b9e5a8 100644 --- a/app/src/main/res/layout/fragment_set_main_credential.xml +++ b/app/src/main/res/layout/fragment_set_main_credential.xml @@ -131,13 +131,11 @@ - + app:layout_constraintEnd_toEndOf="parent" + android:importantForAccessibility="no" + android:importantForAutofill="no" /> From 327c9de46497a31e387143f6d02546d3bc717f10 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Tue, 10 May 2022 19:59:56 +0200 Subject: [PATCH 009/171] Change main credential validation --- .../SetMainCredentialDialogFragment.kt | 116 +++++++++++------- .../database/action/DatabaseTaskProvider.kt | 11 +- .../hardware/HardwareKeyResponseHelper.kt | 93 ++++++++------ .../DatabaseTaskNotificationService.kt | 6 +- .../com/kunzisoft/keepass/utils/UriUtil.kt | 6 +- .../keepass/view/HardwareKeySelectionView.kt | 15 ++- .../keepass/view/MainCredentialView.kt | 14 ++- .../layout/view_hardware_key_selection.xml | 2 +- app/src/main/res/values/strings.xml | 2 + 9 files changed, 169 insertions(+), 96 deletions(-) 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 f9f6c9a4b..dd3bd064e 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 @@ -36,6 +36,7 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.hardware.HardwareKey +import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.utils.UriUtil @@ -53,7 +54,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private lateinit var rootView: View private lateinit var passwordCheckBox: CompoundButton - private lateinit var passKeyView: PassKeyView + private lateinit var passwordView: PassKeyView private lateinit var passwordRepeatTextInputLayout: TextInputLayout private lateinit var passwordRepeatView: TextView @@ -139,7 +140,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } passwordCheckBox = rootView.findViewById(R.id.password_checkbox) - passKeyView = rootView.findViewById(R.id.password_view) + 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() @@ -165,8 +166,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper) - hardwareKeySelectionView.selectionListener = { _ -> + hardwareKeySelectionView.selectionListener = { hardwareKey -> hardwareKeyCheckBox.isChecked = true + hardwareKeySelectionView.error = + if (!HardwareKeyResponseHelper.isHardwareKeyAvailable(requireActivity(), hardwareKey)) { + // show hardware driver dialog if required + getString(R.string.warning_hardware_key_required) + } else { + null + } } val dialog = builder.create() @@ -194,18 +202,41 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } private fun approveMainCredential() { - var error = verifyPassword() || verifyKeyFile() || verifyHardwareKey() - if (!passwordCheckBox.isChecked - && !keyFileCheckBox.isChecked - && !hardwareKeyCheckBox.isChecked + 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) + if (mAllowNoMasterKey) { + // show no key dialog if required showNoKeyConfirmationDialog() - else { + } 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 + && !HardwareKeyResponseHelper.isHardwareKeyAvailable( + requireActivity(), hardwareKey, false) + ) { + // show hardware driver dialog if required + error = true + hardwareKeySelectionView.error = getString(R.string.warning_hardware_key_required) } if (!error) { mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) @@ -213,30 +244,11 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { } } - 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 - passKeyView.addTextChangedListener(passwordTextWatcher) - } - - override fun onPause() { - super.onPause() - - passKeyView.removeTextChangedListener(passwordTextWatcher) - } - private fun verifyPassword(): Boolean { var error = false + passwordRepeatTextInputLayout.error = null if (passwordCheckBox.isChecked) { - mMasterPassword = passKeyView.passwordString + mMasterPassword = passwordView.passwordString val confPassword = passwordRepeatView.text.toString() // Verify that passwords match @@ -245,21 +257,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { // Passwords do not match passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match) } - - if ((mMasterPassword.isNullOrEmpty()) - && (!keyFileCheckBox.isChecked - || keyFileSelectionView.uri == null) - && (!hardwareKeyCheckBox.isChecked - || hardwareKeySelectionView.hardwareKey == null)) { - error = true - showEmptyPasswordConfirmationDialog() - } } return error } private fun verifyKeyFile(): Boolean { var error = false + keyFileSelectionView.error = null if (keyFileCheckBox.isChecked) { keyFileSelectionView.uri?.let { uri -> mKeyFileUri = uri @@ -273,25 +277,45 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() { private fun verifyHardwareKey(): Boolean { var error = false + hardwareKeySelectionView.error = null if (hardwareKeyCheckBox.isChecked) { - hardwareKeySelectionView.hardwareKey.let { hardwareKey -> + hardwareKeySelectionView.hardwareKey?.let { hardwareKey -> mHardwareKey = hardwareKey + } ?: run { + error = true + hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key) } - // TODO verify drivers - // error = true } 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() 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 235f6f5b2..67a4fdbb2 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 @@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.CipherEncryptDatabase @@ -84,7 +85,7 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION -import kotlinx.coroutines.* +import kotlinx.coroutines.launch import java.util.* /** @@ -134,8 +135,12 @@ class DatabaseTaskProvider { respondToChallengeIfAllowed() } this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener { - override fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) { - mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed) + override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) { + if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) { + mHardwareKeyResponseHelper?.launchChallengeForResponse(hardwareKey, seed) + } else { + throw InvalidCredentialsDatabaseException("Driver for $hardwareKey is required.") + } } } } else { diff --git a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt index 2aff8bdde..844811ced 100644 --- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKeyResponseHelper.kt @@ -4,13 +4,17 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.util.Log -import android.widget.Toast 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 androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.UriUtil +import kotlinx.coroutines.launch class HardwareKeyResponseHelper { @@ -32,14 +36,13 @@ class HardwareKeyResponseHelper { fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?, extra: Bundle?) -> Unit) { val resultCallback = ActivityResultCallback { result -> - Log.d(TAG, "resultCode from ykdroid: " + result.resultCode) if (result.resultCode == Activity.RESULT_OK) { val challengeResponse: ByteArray? = result.data?.getByteArrayExtra("response") - Log.d(TAG, "Response: " + challengeResponse.contentToString()) + Log.d(TAG, "Response form challenge : " + challengeResponse.contentToString()) onChallengeResponded.invoke(challengeResponse, result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) } else { - Log.e(TAG, "Response error") + Log.e(TAG, "Response from challenge error") onChallengeResponded.invoke(null, result.data?.getBundleExtra(EXTRA_BUNDLE_KEY)) } @@ -58,39 +61,25 @@ class HardwareKeyResponseHelper { } } - fun launchChallengeForResponse(hardwareKey: HardwareKey?, seed: ByteArray?) { - try { - when (hardwareKey) { - HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { - // 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 - getChallengeResponseResultLauncher?.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply { - putExtra(YKDROID_SEED_KEY, challenge) - }) - Log.d(TAG, "Challenge sent : " + challenge.contentToString()) - } - else -> { - // TODO other algorithm - } + fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) { + when (hardwareKey) { + HardwareKey.FIDO2_SECRET -> { + // TODO FIDO2 + throw Exception("FIDO2 not implemented") } - } catch (e: Exception) { - Log.e( - TAG, - "Unable to retrieve the challenge response", - e - ) - e.message?.let { message -> - Toast.makeText( - activity, - message, - Toast.LENGTH_LONG - ).show() + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { + // 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 + getChallengeResponseResultLauncher!!.launch(Intent(YKDROID_CHALLENGE_RESPONSE_INTENT).apply { + putExtra(YKDROID_SEED_KEY, challenge) + }) + Log.d(TAG, "Challenge sent : " + challenge.contentToString()) } } } @@ -98,9 +87,39 @@ class HardwareKeyResponseHelper { companion object { private val TAG = HardwareKeyResponseHelper::class.java.simpleName - private const val YKDROID_CHALLENGE_RESPONSE_INTENT = "net.pp3345.ykdroid.intent.action.CHALLENGE_RESPONSE" + private const val YKDROID_PACKAGE = "net.pp3345.ykdroid" + private const val YKDROID_CHALLENGE_RESPONSE_INTENT = + "$YKDROID_PACKAGE.intent.action.CHALLENGE_RESPONSE" private const val YKDROID_SEED_KEY = "challenge" private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY" + fun isHardwareKeyAvailable(activity: FragmentActivity, + hardwareKey: HardwareKey, + showDialog: Boolean = true): Boolean { + return when (hardwareKey) { + HardwareKey.FIDO2_SECRET -> { + // TODO FIDO2 + if (showDialog) + showHardwareKeyDriverNeeded(activity) + false + } + HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> { + // TODO (UriUtil.isExternalAppInstalled(activity, KEEPASSDX_PRO_PACKAGE) + UriUtil.isExternalAppInstalled(activity, YKDROID_PACKAGE) + } + } + } + + private fun showHardwareKeyDriverNeeded(activity: FragmentActivity) { + activity.lifecycleScope.launch { + val builder = AlertDialog.Builder(activity) + builder.setMessage(R.string.warning_hardware_key_required) + .setPositiveButton(android.R.string.ok) { _, _ -> + UriUtil.openExternalApp(activity, UriUtil.KEEPASSDX_PRO_PACKAGE) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + builder.create().show() + } + } } } \ No newline at end of file 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 4da40c9bd..4c10db941 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -70,7 +70,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private var mActionTaskBinder = ActionTaskBinder() private var mActionTaskListeners = mutableListOf() // Channel to connect asynchronously a listener or a response - private var mRequestChallengeListenerChannel = Channel(0) + private var mRequestChallengeListenerChannel = Channel(0) private var mResponseChallengeChannel = Channel(0) private var mActionRunning = false @@ -156,7 +156,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } interface RequestChallengeListener { - fun onChallengeResponseRequested(hardwareKey: HardwareKey?, seed: ByteArray?) + fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) } fun checkDatabase() { @@ -598,7 +598,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress runBlocking { // Send the request val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive() - challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed) + challengeResponseRequestListener.onChallengeResponseRequested(hardwareKey, seed) // Wait the response response = mResponseChallengeChannel.receive() ?: byteArrayOf() } 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..0286b837e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -269,11 +269,11 @@ object UriUtil { fun contributingUser(context: Context): Boolean { return (Education.isEducationScreenReclickedPerformed(context) - || isExternalAppInstalled(context, "com.kunzisoft.keepass.pro", false) + || isExternalAppInstalled(context, KEEPASSDX_PRO_PACKAGE, 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) @@ -317,4 +317,6 @@ object UriUtil { } private const val TAG = "UriUtil" + + const val KEEPASSDX_PRO_PACKAGE = "com.kunzisoft.keepass.pro" } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt index 32fc7880d..f7d6bd063 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt @@ -12,6 +12,7 @@ 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 @@ -25,8 +26,9 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, private var mHardwareKey: HardwareKey? = null + private val hardwareKeyLayout: TextInputLayout private val hardwareKeyCompletion: AppCompatAutoCompleteTextView - var selectionListener: ((HardwareKey?)-> Unit)? = null + var selectionListener: ((HardwareKey)-> Unit)? = null private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context) @@ -66,6 +68,7 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, 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.inputType = InputType.TYPE_NULL @@ -74,7 +77,9 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, hardwareKeyCompletion.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> mHardwareKey = HardwareKey.fromPosition(position) - selectionListener?.invoke(mHardwareKey) + mHardwareKey?.let { hardwareKey -> + selectionListener?.invoke(hardwareKey) + } } } @@ -88,6 +93,12 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, hardwareKeyCompletion.setSelection(value.ordinal) } + var error: CharSequence? + get() = hardwareKeyLayout.error + set(value) { + hardwareKeyLayout.error = value + } + override fun onSaveInstanceState(): Parcelable { val superState = super.onSaveInstanceState() val saveState = SavedState(superState) 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 584f89428..dbd40684c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -107,9 +107,19 @@ class MainCredentialView @JvmOverloads constructor(context: Context, 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) } @@ -152,8 +162,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context, fun isFill(): Boolean { return checkboxPasswordView.isChecked - || checkboxKeyFileView.isChecked // TODO better recognition - || checkboxHardwareView.isChecked + || (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null) + || (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null) } fun getMainCredential(): MainCredential { diff --git a/app/src/main/res/layout/view_hardware_key_selection.xml b/app/src/main/res/layout/view_hardware_key_selection.xml index 2c441ef5e..09ac78e9b 100644 --- a/app/src/main/res/layout/view_hardware_key_selection.xml +++ b/app/src/main/res/layout/view_hardware_key_selection.xml @@ -12,7 +12,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index adf076d03..83ad082fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,6 +161,7 @@ Enter a name. This word is reserved and cannot be used. Select a keyfile. + Select a hardware key. No memory to load your entire database. Could not load your database. Could not load the key. Try to lower the KDF \"Memory Usage\". @@ -355,6 +356,7 @@ Access to the file revoked by the file manager, close the database and reopen it from its location. You have not allowed the app to use an exact alarm. As a result, the features requiring a timer will not be done with an exact time. The hash of the file is not guaranteed because Android can change its data on the fly. Change the file extension to .bin for correct integrity. + Driver for this hardware is required. Permission Version %1$s Build %1$s From 20e35f1a69a47693692b8a5241037d6ad151f49d Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 11 May 2022 13:42:48 +0200 Subject: [PATCH 010/171] Encapsulate database operations --- .../keepass/database/element/Database.kt | 219 +++--------------- .../database/element/database/DatabaseKDB.kt | 43 +++- .../database/element/database/DatabaseKDBX.kt | 69 +++++- .../element/database/DatabaseVersioned.kt | 23 +- .../database/file/input/DatabaseInput.kt | 2 +- .../database/file/input/DatabaseInputKDB.kt | 4 +- .../database/file/input/DatabaseInputKDBX.kt | 5 +- .../file/output/DatabaseOutputKDBX.kt | 4 +- 8 files changed, 162 insertions(+), 207 deletions(-) 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 12371d532..b5cbd8c0c 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 @@ -26,7 +26,6 @@ import android.graphics.Color import android.net.Uri import android.util.Log import com.kunzisoft.androidclearchroma.ChromaUtil -import com.kunzisoft.encrypt.HashManager import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine @@ -66,7 +65,6 @@ import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.readBytes4ToUInt import java.io.* -import java.nio.charset.Charset import java.util.* @@ -79,10 +77,6 @@ class Database { var fileUri: Uri? = null private set - // To resave the database with same credential when already loaded - private var mMainCredential = MainCredential() - private var mCompositeKey = CompositeKey() - private var mSearchHelper: SearchHelper = SearchHelper() var isReadOnly = false @@ -601,12 +595,10 @@ class Database { DatabaseInputKDB(databaseKDB) .openDatabase(databaseInputStream, progressTaskUpdater - ) { seed -> - databaseKDB.masterKey = deriveKDBMasterKey( - databaseKDB, + ) { + databaseKDB.deriveMasterKey( contentResolver, - mainCredential, - challengeResponseRetriever + mainCredential ) } setDatabaseKDB(databaseKDB) @@ -619,9 +611,8 @@ class Database { DatabaseInputKDBX(databaseKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, - progressTaskUpdater) { seed -> - databaseKDBX.masterKey = deriveKDBXMasterKey( - databaseKDBX, + progressTaskUpdater) { + databaseKDBX.deriveMasterKey( contentResolver, mainCredential, challengeResponseRetriever @@ -670,17 +661,16 @@ class Database { { databaseInputStream -> val databaseToMergeKDB = DatabaseKDB() DatabaseInputKDB(databaseToMergeKDB) - .openDatabase(databaseInputStream, progressTaskUpdater) { seed -> + .openDatabase(databaseInputStream, progressTaskUpdater) { if (databaseToMergeMainCredential != null) { - databaseToMergeKDB.masterKey = deriveKDBMasterKey( - databaseToMergeKDB, + databaseToMergeKDB.deriveMasterKey( contentResolver, - databaseToMergeMainCredential, - databaseToMergeChallengeResponseRetriever + databaseToMergeMainCredential ) } else { - databaseToMergeKDB.masterKey = masterKey - databaseToMergeKDB.transformSeed = transformSeed + this@Database.mDatabaseKDB?.let { thisDatabaseKDB -> + databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB) + } } } setDatabaseKDB(databaseToMergeKDB) @@ -689,18 +679,17 @@ class Database { val databaseToMergeKDBX = DatabaseKDBX() DatabaseInputKDBX(databaseToMergeKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) - openDatabase(databaseInputStream, progressTaskUpdater) { seed -> - // TODO challenge response + openDatabase(databaseInputStream, progressTaskUpdater) { if (databaseToMergeMainCredential != null) { - databaseToMergeKDBX.masterKey = deriveKDBXMasterKey( - databaseToMergeKDBX, + databaseToMergeKDBX.deriveMasterKey( contentResolver, databaseToMergeMainCredential, databaseToMergeChallengeResponseRetriever ) } else { - databaseToMergeKDBX.masterKey = masterKey - databaseToMergeKDBX.transformSeed = transformSeed + this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX -> + databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX) + } } } } @@ -757,8 +746,9 @@ class Database { } DatabaseInputKDB(databaseKDB) .openDatabase(databaseInputStream, progressTaskUpdater) { - databaseKDB.masterKey = masterKey - databaseKDB.transformSeed = transformSeed + this@Database.mDatabaseKDB?.let { thisDatabaseKDB -> + databaseKDB.copyMasterKeyFrom(thisDatabaseKDB) + } } setDatabaseKDB(databaseKDB) }, @@ -770,8 +760,9 @@ class Database { DatabaseInputKDBX(databaseKDBX).apply { setMethodToCheckIfRAMIsSufficient(isRAMSufficient) openDatabase(databaseInputStream, progressTaskUpdater) { - databaseKDBX.masterKey = masterKey - databaseKDBX.transformSeed = transformSeed + this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX -> + databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX) + } } } setDatabaseKDBX(databaseKDBX) @@ -807,41 +798,20 @@ class Database { DatabaseOutputKDB(it, definedOutputStream) } ?: mDatabaseKDBX?.let { - DatabaseOutputKDBX(it, definedOutputStream) { seed -> - var masterKeyDerived : ByteArray? = null + DatabaseOutputKDBX(it, definedOutputStream) { if (mainCredential != null) { - // If composite key is null, build byte array from MainCredential - mDatabaseKDB?.let { databaseKDB -> - masterKeyDerived = deriveKDBMasterKey( - databaseKDB, - contentResolver, - mainCredential, - challengeResponseRetriever - ) - } - mDatabaseKDBX?.let { databaseKDBX -> - masterKeyDerived = deriveKDBXMasterKey( - databaseKDBX, - contentResolver, - mainCredential, - challengeResponseRetriever - ) - } + // Build new master key from MainCredential + mDatabaseKDBX?.deriveMasterKey( + contentResolver, + mainCredential, + challengeResponseRetriever + ) } else { // Reuse composite key parts - mDatabaseKDB?.let { databaseKDB -> - masterKeyDerived = databaseKDB.masterKey - } - mDatabaseKDBX?.let { databaseKDBX -> - masterKeyDerived = deriveKDBXCompositeKey( - mMainCredential, - mCompositeKey, - challengeResponseRetriever - ) - } + mDatabaseKDBX?.deriveCompositeKey( + challengeResponseRetriever + ) } - masterKeyDerived ?: - throw DatabaseOutputException("Unable to derive master key without database.") } } databaseOutput?.output() @@ -861,100 +831,6 @@ class Database { } } - @Throws(DatabaseOutputException::class) - private fun deriveKDBMasterKey( - databaseKDB: DatabaseKDB, - contentResolver: ContentResolver, - mainCredential: MainCredential, - challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray - ): ByteArray { - if (mainCredential.password == null && mainCredential.keyFileUri == null) - throw IllegalArgumentException("Key cannot be empty.") - if (mainCredential.hardwareKey != null) - throw IllegalArgumentException("Hardware key is not supported.") - return compositeKeyToMasterKey(retrieveCompositeKey( - contentResolver, - mainCredential, - databaseKDB.passwordEncoding, - databaseKDB.allowXMLKeyFile, - databaseKDB.transformSeed, - challengeResponseRetriever - )) - } - - @Throws(DatabaseOutputException::class) - private fun deriveKDBXMasterKey( - databaseKDBX: DatabaseKDBX, - contentResolver: ContentResolver, - mainCredential: MainCredential, - challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray - ): ByteArray { - return compositeKeyToMasterKey(retrieveCompositeKey( - contentResolver, - mainCredential, - databaseKDBX.passwordEncoding, - databaseKDBX.allowXMLKeyFile, - databaseKDBX.transformSeed, - challengeResponseRetriever - )) - } - - @Throws(DatabaseOutputException::class) - private fun deriveKDBXCompositeKey( - mainCredential: MainCredential, - compositeKey: CompositeKey, - challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray - ): ByteArray { - if (compositeKey.hardwareKeyData == null) - return compositeKeyToMasterKey(compositeKey) - val hardwareKey = mainCredential.hardwareKey - val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( - challengeResponseRetriever.invoke(hardwareKey, transformSeed) - ) else null - return compositeKeyToMasterKey(compositeKey.apply { - this.hardwareKeyData = hardwareKeyBytes - }) - } - - @Throws(IOException::class) - private fun retrieveCompositeKey(contentResolver: ContentResolver, - mainCredential: MainCredential, - passwordEncoding: Charset, - allowXMLKeyFile: Boolean, - transformSeed: ByteArray?, - challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, - ): CompositeKey { - // Save to rebuild master password with new seed later - mMainCredential = mainCredential - val password = mainCredential.password - val keyFileUri = mainCredential.keyFileUri - val hardwareKey = mainCredential.hardwareKey - val passwordBytes = if (password != null) CompositeKey.retrievePasswordKey( - password, - passwordEncoding - ) else null - val keyFileBytes = if (keyFileUri != null) CompositeKey.retrieveFileKey( - contentResolver, - keyFileUri, - allowXMLKeyFile - ) else null - val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( - challengeResponseRetriever.invoke(hardwareKey, transformSeed) - ) else null - val compositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKeyBytes) - // Save to rebuild master password with new seed later - mCompositeKey = compositeKey - return compositeKey - } - - private fun compositeKeyToMasterKey(compositeKey: CompositeKey): ByteArray { - return HashManager.hashSha256( - compositeKey.passwordData, - compositeKey.keyFileData, - compositeKey.hardwareKeyData - ) - } - fun groupIsInRecycleBin(group: Group): Boolean { val groupKDB = group.groupKDB val groupKDBX = group.groupKDBX @@ -1067,8 +943,6 @@ class Database { fun clearAndClose(context: Context? = null) { clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) }) - this.mMainCredential = MainCredential() - this.mCompositeKey = CompositeKey() this.mDatabaseKDB = null this.mDatabaseKDBX = null this.fileUri = null @@ -1483,34 +1357,5 @@ class Database { } } } - - @Throws(LoadDatabaseException::class) - fun getTransformSeed( - contentResolver: ContentResolver, - databaseUri: Uri, - onTransformSeedRetrieved: (ByteArray?) -> Unit - ) { - try { - // Read database stream for the first time - readDatabaseStream( - contentResolver, databaseUri, - { _ -> - }, - { databaseKDBXInputStream -> - val header = DatabaseHeaderKDBX(DatabaseKDBX()) - header.loadFromFile(databaseKDBXInputStream) - val transformSeed = ByteArray(64) - header.transformSeed?.copyInto(transformSeed, 0, 0, 32) - // seed: 32 byte transform seed, needs to be padded before sent to the hardware - transformSeed.fill(32, 32, 64) - onTransformSeedRetrieved(transformSeed) - } - ) - } catch (e: LoadDatabaseException) { - throw e - } catch (e: Exception) { - throw LoadDatabaseException(e) - } - } } } 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 cfd547fc9..aeaf611ee 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.CompositeKey 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,6 +33,7 @@ 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.model.MainCredential import java.io.IOException import java.nio.charset.Charset import java.util.* @@ -59,9 +62,6 @@ class DatabaseKDB : DatabaseVersioned() { override val passwordEncoding: Charset get() = Charsets.ISO_8859_1 - override val allowXMLKeyFile: Boolean - get() = false - override var numberKeyEncryptionRounds = 300L override val version: String @@ -127,6 +127,43 @@ class DatabaseKDB : DatabaseVersioned() { finalKey = HashManager.hashSha256(masterSeed, transformedKey) } + fun deriveMasterKey( + contentResolver: ContentResolver, + mainCredential: MainCredential + ) { + if (mainCredential.password == null && mainCredential.keyFileUri == null) + throw IllegalArgumentException("Key cannot be empty.") + if (mainCredential.hardwareKey != null) + throw IllegalArgumentException("Hardware key is not supported.") + this.masterKey = compositeKeyToMasterKey(retrieveCompositeKey( + contentResolver, + mainCredential + )) + } + + @Throws(IOException::class) + private fun retrieveCompositeKey(contentResolver: ContentResolver, + mainCredential: MainCredential + ): CompositeKey { + // Save to rebuild master password with new seed later + mMainCredential = mainCredential + val password = mainCredential.password + val keyFileUri = mainCredential.keyFileUri + val passwordBytes = if (password != null) CompositeKey.retrievePasswordKey( + password, + passwordEncoding + ) else null + val keyFileBytes = if (keyFileUri != null) CompositeKey.retrieveFileKey( + contentResolver, + keyFileUri, + false + ) else null + val compositeKey = CompositeKey(passwordBytes, keyFileBytes) + // Save to rebuild master password with new seed later + mCompositeKey = compositeKey + return compositeKey + } + 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 5607c6dca..b568cd76b 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,9 +47,12 @@ 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.hardware.HardwareKey +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.longTo8Bytes import java.io.IOException @@ -225,6 +226,63 @@ class DatabaseKDBX : DatabaseVersioned { } } + fun deriveMasterKey( + contentResolver: ContentResolver, + mainCredential: MainCredential, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ) { + this.masterKey = compositeKeyToMasterKey(retrieveCompositeKey( + contentResolver, + mainCredential, + transformSeed, + challengeResponseRetriever + )) + } + + @Throws(DatabaseOutputException::class) + fun deriveCompositeKey( + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray + ) { + if (mCompositeKey.hardwareKeyData == null) + this.masterKey = compositeKeyToMasterKey(mCompositeKey) + val hardwareKey = mMainCredential.hardwareKey + val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( + challengeResponseRetriever.invoke(hardwareKey, transformSeed) + ) else null + this.masterKey = compositeKeyToMasterKey(mCompositeKey.apply { + this.hardwareKeyData = hardwareKeyBytes + }) + } + + @Throws(IOException::class) + private fun retrieveCompositeKey(contentResolver: ContentResolver, + mainCredential: MainCredential, + transformSeed: ByteArray?, + challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, + ): CompositeKey { + // Save to rebuild master password with new seed later + mMainCredential = mainCredential + val password = mainCredential.password + val keyFileUri = mainCredential.keyFileUri + val hardwareKey = mainCredential.hardwareKey + val passwordBytes = if (password != null) CompositeKey.retrievePasswordKey( + password, + passwordEncoding + ) else null + val keyFileBytes = if (keyFileUri != null) CompositeKey.retrieveFileKey( + contentResolver, + keyFileUri, + true + ) else null + val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( + challengeResponseRetriever.invoke(hardwareKey, transformSeed) + ) else null + val compositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKeyBytes) + // Save to rebuild master password with new seed later + mCompositeKey = compositeKey + return compositeKey + } + fun getMinKdbxVersion(): UnsignedInt { val entryHandler = EntryOperationHandler() val groupHandler = GroupOperationHandler() @@ -359,9 +417,6 @@ class DatabaseKDBX : DatabaseVersioned { override val passwordEncoding: Charset get() = Charsets.UTF_8 - override val allowXMLKeyFile: Boolean - get() = true - private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? { if (groupUUID == UUID_ZERO) return null 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 9b6686378..182475e9d 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,8 +20,10 @@ 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.CompositeKey import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.entry.EntryVersioned @@ -31,6 +33,7 @@ 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 com.kunzisoft.keepass.model.MainCredential import java.io.InputStream import java.io.UnsupportedEncodingException import java.nio.charset.Charset @@ -43,7 +46,6 @@ abstract class DatabaseVersioned< Entry : EntryVersioned > { - // Algorithm used to encrypt the database abstract var encryptionAlgorithm: EncryptionAlgorithm abstract val availableEncryptionAlgorithms: List @@ -53,12 +55,14 @@ abstract class DatabaseVersioned< abstract var numberKeyEncryptionRounds: Long abstract val passwordEncoding: Charset - abstract val allowXMLKeyFile: Boolean var masterKey = ByteArray(32) var finalKey: ByteArray? = null protected set + // To resave the database with same credential when already loaded + protected var mMainCredential = MainCredential() + protected var mCompositeKey = CompositeKey() var transformSeed: ByteArray? = null abstract val version: String @@ -120,6 +124,21 @@ abstract class DatabaseVersioned< return password == reEncoded } + fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned) { + this.masterKey = databaseVersioned.masterKey + this.mMainCredential = databaseVersioned.mMainCredential + this.mCompositeKey = databaseVersioned.mCompositeKey + this.transformSeed = databaseVersioned.transformSeed + } + + protected fun compositeKeyToMasterKey(compositeKey: CompositeKey): ByteArray { + return HashManager.hashSha256( + compositeKey.passwordData, + compositeKey.keyFileData, + compositeKey.hardwareKeyData + ) + } + /* * ------------------------------------- * Node Creation 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 20d13eec9..321876af4 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 @@ -44,7 +44,7 @@ abstract class DatabaseInput> (protected var m @Throws(LoadDatabaseException::class) abstract fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: ((seed: ByteArray?) -> Unit)): D + assignMasterKey: (() -> Unit)): D protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) 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 556987430..e107f6c18 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 @@ -50,7 +50,7 @@ class DatabaseInputKDB(database: DatabaseKDB) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: ((seed: ByteArray?) -> Unit)): DatabaseKDB { + assignMasterKey: (() -> Unit)): DatabaseKDB { try { startKeyTimer(progressTaskUpdater) @@ -77,7 +77,7 @@ class DatabaseInputKDB(database: DatabaseKDB) } mDatabase.transformSeed = header.transformSeed - assignMasterKey.invoke(header.transformSeed) + assignMasterKey.invoke() // Select algorithm when { 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 56aa5daf5..1cb5b910a 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 @@ -46,7 +46,6 @@ import com.kunzisoft.keepass.stream.HashedBlockInputStream import com.kunzisoft.keepass.stream.HmacBlockInputStream import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.* -import kotlinx.coroutines.yield import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory @@ -103,7 +102,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) @Throws(LoadDatabaseException::class) override fun openDatabase(databaseInputStream: InputStream, progressTaskUpdater: ProgressTaskUpdater?, - assignMasterKey: ((seed: ByteArray?) -> Unit)): DatabaseKDBX { + assignMasterKey: (() -> Unit)): DatabaseKDBX { try { startKeyTimer(progressTaskUpdater) @@ -117,7 +116,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX) val transformSeed = header.transformSeed mDatabase.transformSeed = transformSeed - assignMasterKey.invoke(transformSeed) + assignMasterKey.invoke() mDatabase.makeFinalKey(header.masterSeed) stopKeyTimer() 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 7caf878da..360ea8fb3 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 @@ -58,7 +58,7 @@ import kotlin.experimental.or class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, outputStream: OutputStream, - private val mAssignMasterKey: ((seed: ByteArray?) -> ByteArray)) + private val mAssignMasterKey: (() -> Unit)) : DatabaseOutput(outputStream) { private var randomStream: StreamCipher? = null @@ -329,7 +329,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, setIVs(header) // TODO Check modification - mDatabaseKDBX.masterKey = mAssignMasterKey.invoke(header.transformSeed) + mAssignMasterKey.invoke() val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream) pho.output() From 20b352cabe33a590b4232a8b7c811633c5bea590 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 11 May 2022 14:19:32 +0200 Subject: [PATCH 011/171] Better code encapsulation --- .../activities/FileDatabaseSelectActivity.kt | 2 +- .../keepass/activities/GroupActivity.kt | 2 +- .../activities/MainCredentialActivity.kt | 1 + .../dialogs/MainCredentialDialogFragment.kt | 2 +- .../dialogs/PasswordEncodingDialogFragment.kt | 2 +- .../SetMainCredentialDialogFragment.kt | 2 +- .../activities/legacy/DatabaseActivity.kt | 2 +- .../activities/legacy/DatabaseLockActivity.kt | 2 +- .../AssignMainCredentialInDatabaseRunnable.kt | 12 +- .../database/action/CreateDatabaseRunnable.kt | 2 +- .../database/action/DatabaseTaskProvider.kt | 8 +- .../database/action/LoadDatabaseRunnable.kt | 2 +- .../database/action/MergeDatabaseRunnable.kt | 2 +- .../database/action/SaveDatabaseRunnable.kt | 2 +- .../keepass/database/element/CompositeKey.kt | 211 +------------ .../keepass/database/element/Database.kt | 1 - .../database/element/MainCredential.kt | 276 ++++++++++++++++++ .../database/element/database/DatabaseKDB.kt | 30 +- .../database/element/database/DatabaseKDBX.kt | 103 ++++--- .../element/database/DatabaseVersioned.kt | 17 -- .../kunzisoft/keepass/model/MainCredential.kt | 77 ----- .../DatabaseTaskNotificationService.kt | 2 +- .../keepass/settings/SettingsActivity.kt | 2 +- .../keepass/view/MainCredentialView.kt | 2 +- 24 files changed, 378 insertions(+), 386 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/database/element/MainCredential.kt delete mode 100644 app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt 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..e001841ed 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,7 @@ 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.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService 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..e20bcd8fe 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 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 bdf85beb0..956932f11 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -57,6 +57,7 @@ import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockManager 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.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation 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 2180caa8a..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 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/SetMainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetMainCredentialDialogFragment.kt index dd3bd064e..dab99bae6 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 @@ -37,7 +37,7 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper -import com.kunzisoft.keepass.model.MainCredential +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 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 802967e3b..58857caf8 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 @@ -7,7 +7,7 @@ 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.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.viewmodels.DatabaseViewModel 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 14e28631e..415f75807 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 @@ -43,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 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 06478eef3..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 @@ -25,14 +25,14 @@ 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.hardware.HardwareKey -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential open class AssignMainCredentialInDatabaseRunnable ( - context: Context, - database: Database, - protected val mDatabaseUri: Uri, - mainCredential: MainCredential, - challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) + 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 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 52a36bd98..bf3a53954 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 @@ -25,7 +25,7 @@ import android.util.Log import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.hardware.HardwareKey -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil class CreateDatabaseRunnable(context: Context, 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 67a4fdbb2..c52d4d63d 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 @@ -46,7 +46,7 @@ import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseExcept import com.kunzisoft.keepass.hardware.HardwareKey import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK @@ -394,7 +394,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) @@ -447,7 +448,8 @@ class DatabaseTaskProvider { } fun startDatabaseAssignPassword(databaseUri: Uri, - mainCredential: MainCredential) { + mainCredential: MainCredential + ) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) 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 0ce8d3d5b..1051cc541 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 @@ -28,7 +28,7 @@ import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.exception.LoadDatabaseException 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 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 a3c925d8e..f2b2d3274 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 @@ -25,7 +25,7 @@ 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.hardware.HardwareKey -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 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 027d4c319..5fdc65e82 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 @@ -24,7 +24,7 @@ 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.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable open class SaveDatabaseRunnable(protected var context: Context, 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 index 3a429c92c..6423068f9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/CompositeKey.kt @@ -1,28 +1,10 @@ package com.kunzisoft.keepass.database.element -import android.content.ContentResolver -import android.net.Uri -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.utils.StringUtil.removeSpaceChars -import com.kunzisoft.keepass.utils.StringUtil.toHexString -import com.kunzisoft.keepass.utils.UriUtil -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 +import com.kunzisoft.keepass.hardware.HardwareKey data class CompositeKey(var passwordData: ByteArray? = null, var keyFileData: ByteArray? = null, - var hardwareKeyData: ByteArray? = null) { + var hardwareKey: HardwareKey? = null) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -38,10 +20,7 @@ data class CompositeKey(var passwordData: ByteArray? = null, if (other.keyFileData == null) return false if (!keyFileData.contentEquals(other.keyFileData)) return false } else if (other.keyFileData != null) return false - if (hardwareKeyData != null) { - if (other.hardwareKeyData == null) return false - if (!hardwareKeyData.contentEquals(other.hardwareKeyData)) return false - } else if (other.hardwareKeyData != null) return false + if (hardwareKey != other.hardwareKey) return false return true } @@ -49,189 +28,7 @@ data class CompositeKey(var passwordData: ByteArray? = null, override fun hashCode(): Int { var result = passwordData?.contentHashCode() ?: 0 result = 31 * result + (keyFileData?.contentHashCode() ?: 0) - result = 31 * result + (hardwareKeyData?.contentHashCode() ?: 0) + result = 31 * result + (hardwareKey?.hashCode() ?: 0) return result } - - companion object { - - private val TAG = CompositeKey::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" - } } \ 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 b5cbd8c0c..62c964d75 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 @@ -59,7 +59,6 @@ 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 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 aeaf611ee..c72817dcb 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 @@ -25,7 +25,6 @@ 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.CompositeKey import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.group.GroupKDB @@ -33,7 +32,7 @@ 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.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import java.io.IOException import java.nio.charset.Charset import java.util.* @@ -131,37 +130,30 @@ class DatabaseKDB : DatabaseVersioned() { contentResolver: ContentResolver, mainCredential: MainCredential ) { + // Exception when no password if (mainCredential.password == null && mainCredential.keyFileUri == null) throw IllegalArgumentException("Key cannot be empty.") if (mainCredential.hardwareKey != null) throw IllegalArgumentException("Hardware key is not supported.") - this.masterKey = compositeKeyToMasterKey(retrieveCompositeKey( - contentResolver, - mainCredential - )) - } - @Throws(IOException::class) - private fun retrieveCompositeKey(contentResolver: ContentResolver, - mainCredential: MainCredential - ): CompositeKey { - // Save to rebuild master password with new seed later - mMainCredential = mainCredential + // Retrieve plain data val password = mainCredential.password val keyFileUri = mainCredential.keyFileUri - val passwordBytes = if (password != null) CompositeKey.retrievePasswordKey( + val passwordBytes = if (password != null) MainCredential.retrievePasswordKey( password, passwordEncoding ) else null - val keyFileBytes = if (keyFileUri != null) CompositeKey.retrieveFileKey( + val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey( contentResolver, keyFileUri, false ) else null - val compositeKey = CompositeKey(passwordBytes, keyFileBytes) - // Save to rebuild master password with new seed later - mCompositeKey = compositeKey - return compositeKey + + // Build master key + this.masterKey = HashManager.hashSha256( + passwordBytes, + keyFileBytes + ) } override fun createGroup(): 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 b568cd76b..1c2e4a181 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 @@ -52,7 +52,7 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VER 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.hardware.HardwareKey -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.longTo8Bytes import java.io.IOException @@ -66,6 +66,9 @@ 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 @@ -231,56 +234,72 @@ class DatabaseKDBX : DatabaseVersioned { mainCredential: MainCredential, challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray ) { - this.masterKey = compositeKeyToMasterKey(retrieveCompositeKey( + // 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, - mainCredential, - transformSeed, - challengeResponseRetriever - )) + 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 ) { - if (mCompositeKey.hardwareKeyData == null) - this.masterKey = compositeKeyToMasterKey(mCompositeKey) - val hardwareKey = mMainCredential.hardwareKey - val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( - challengeResponseRetriever.invoke(hardwareKey, transformSeed) - ) else null - this.masterKey = compositeKeyToMasterKey(mCompositeKey.apply { - this.hardwareKeyData = hardwareKeyBytes - }) + 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 + ) + } } - @Throws(IOException::class) - private fun retrieveCompositeKey(contentResolver: ContentResolver, - mainCredential: MainCredential, - transformSeed: ByteArray?, - challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray, - ): CompositeKey { - // Save to rebuild master password with new seed later - mMainCredential = mainCredential - val password = mainCredential.password - val keyFileUri = mainCredential.keyFileUri - val hardwareKey = mainCredential.hardwareKey - val passwordBytes = if (password != null) CompositeKey.retrievePasswordKey( - password, - passwordEncoding - ) else null - val keyFileBytes = if (keyFileUri != null) CompositeKey.retrieveFileKey( - contentResolver, - keyFileUri, - true - ) else null - val hardwareKeyBytes = if (hardwareKey != null) CompositeKey.retrieveHardwareKey( - challengeResponseRetriever.invoke(hardwareKey, transformSeed) - ) else null - val compositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKeyBytes) - // Save to rebuild master password with new seed later - mCompositeKey = compositeKey - return compositeKey + 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 { 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 182475e9d..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,10 +20,8 @@ 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.CompositeKey import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.entry.EntryVersioned @@ -33,7 +31,6 @@ 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 com.kunzisoft.keepass.model.MainCredential import java.io.InputStream import java.io.UnsupportedEncodingException import java.nio.charset.Charset @@ -59,10 +56,6 @@ abstract class DatabaseVersioned< var masterKey = ByteArray(32) var finalKey: ByteArray? = null protected set - - // To resave the database with same credential when already loaded - protected var mMainCredential = MainCredential() - protected var mCompositeKey = CompositeKey() var transformSeed: ByteArray? = null abstract val version: String @@ -126,19 +119,9 @@ abstract class DatabaseVersioned< fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned) { this.masterKey = databaseVersioned.masterKey - this.mMainCredential = databaseVersioned.mMainCredential - this.mCompositeKey = databaseVersioned.mCompositeKey this.transformSeed = databaseVersioned.transformSeed } - protected fun compositeKeyToMasterKey(compositeKey: CompositeKey): ByteArray { - return HashManager.hashSha256( - compositeKey.passwordData, - compositeKey.keyFileData, - compositeKey.hardwareKeyData - ) - } - /* * ------------------------------------- * Node Creation 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 8a7a3c479..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.model - -import android.net.Uri -import android.os.Parcel -import android.os.Parcelable -import com.kunzisoft.keepass.hardware.HardwareKey -import com.kunzisoft.keepass.utils.readEnum -import com.kunzisoft.keepass.utils.writeEnum - -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) - } - } -} 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 4c10db941..5f1e37be0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -43,7 +43,7 @@ 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.model.CipherEncryptDatabase -import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater 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 932dbce0f..e2438d5bf 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -38,7 +38,7 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper -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/view/MainCredentialView.kt b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt index dbd40684c..34a7f2786 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -39,7 +39,7 @@ 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 class MainCredentialView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, From f4d5bd1bea124b1907c8af38d16d37a63984fed8 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 11 May 2022 15:26:49 +0200 Subject: [PATCH 012/171] Fix save and better write implementation --- .../activities/MainCredentialActivity.kt | 8 ------ .../activities/legacy/DatabaseActivity.kt | 9 +++---- .../keepass/database/element/Database.kt | 27 +++++++++++++------ .../database/element/database/DatabaseKDBX.kt | 1 - .../database/file/output/DatabaseOutput.kt | 8 +++--- .../database/file/output/DatabaseOutputKDB.kt | 18 ++++++++----- .../file/output/DatabaseOutputKDBX.kt | 26 +++++++++--------- .../DatabaseTaskNotificationService.kt | 3 ++- .../keepass/settings/SettingsActivity.kt | 8 ------ 9 files changed, 52 insertions(+), 56 deletions(-) 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 956932f11..8edeaaf5b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -55,13 +55,11 @@ import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockManager -import com.kunzisoft.keepass.database.action.DatabaseTaskProvider 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.HardwareKeyResponseHelper 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 @@ -107,17 +105,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false - private var mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) - private var mAutofillActivityResultLauncher: ActivityResultLauncher? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) AutofillHelper.buildActivityResultLauncher(this) else null - override fun initializeDatabaseTaskProvider(): DatabaseTaskProvider { - return DatabaseTaskProvider(this, mHardwareKeyResponseHelper) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 58857caf8..0bc31fb8c 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 @@ -8,6 +8,7 @@ import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.database.element.MainCredential +import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.viewmodels.DatabaseViewModel @@ -17,10 +18,12 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null protected var mDatabase: Database? = null + private var mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mDatabaseTaskProvider = initializeDatabaseTaskProvider() + mDatabaseTaskProvider = DatabaseTaskProvider(this, mHardwareKeyResponseHelper) mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> val databaseWasReloaded = database?.wasReloaded == true @@ -36,10 +39,6 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { } } - open fun initializeDatabaseTaskProvider(): DatabaseTaskProvider { - return DatabaseTaskProvider(this) - } - override fun onDatabaseRetrieved(database: Database?) { mDatabase = database mDatabaseViewModel.defineDatabase(database) 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 62c964d75..4a6d2b7ad 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 @@ -792,28 +792,39 @@ class Database { try { outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri) outputStream?.let { definedOutputStream -> - val databaseOutput = - mDatabaseKDB?.let { - DatabaseOutputKDB(it, definedOutputStream) + mDatabaseKDB?.let { databaseKDB -> + DatabaseOutputKDB(databaseKDB).apply { + writeDatabase(definedOutputStream) { + if (mainCredential != null) { + databaseKDB.deriveMasterKey( + contentResolver, + mainCredential + ) + } else { + // No master key change + } + } } - ?: mDatabaseKDBX?.let { - DatabaseOutputKDBX(it, definedOutputStream) { + } + ?: mDatabaseKDBX?.let { databaseKDBX -> + DatabaseOutputKDBX(databaseKDBX).apply { + writeDatabase(definedOutputStream) { if (mainCredential != null) { // Build new master key from MainCredential - mDatabaseKDBX?.deriveMasterKey( + databaseKDBX.deriveMasterKey( contentResolver, mainCredential, challengeResponseRetriever ) } else { // Reuse composite key parts - mDatabaseKDBX?.deriveCompositeKey( + databaseKDBX.deriveCompositeKey( challengeResponseRetriever ) } } } - databaseOutput?.output() + } } } catch (e: Exception) { throw IOException(e) 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 1c2e4a181..7402fc529 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 @@ -52,7 +52,6 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VER 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.hardware.HardwareKey -import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.longTo8Bytes import java.io.IOException 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 da556654d..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,14 +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 { @@ -80,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() @@ -106,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 @@ -131,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 360ea8fb3..ca1966e13 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,10 +56,8 @@ import javax.crypto.CipherOutputStream import kotlin.experimental.or -class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, - outputStream: OutputStream, - private val mAssignMasterKey: (() -> Unit)) - : DatabaseOutput(outputStream) { +class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX) + : DatabaseOutput() { private var randomStream: StreamCipher? = null private lateinit var xml: XmlSerializer @@ -68,21 +66,22 @@ 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 @@ -323,13 +322,14 @@ 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) - // TODO Check modification - mAssignMasterKey.invoke() + mDatabaseKDBX.transformSeed = header.transformSeed + assignMasterKey.invoke() val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream) pho.output() 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 5f1e37be0..8e7427e13 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -135,7 +135,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun removeRequestChallengeListener(requestChallengeListener: RequestChallengeListener) { mainScope.launch { - // TODO mRequestChallengeListenerChannel.cancel() + mRequestChallengeListenerChannel.cancel() + mRequestChallengeListenerChannel = Channel(0) } } } 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 e2438d5bf..3ddb6d60c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -35,9 +35,7 @@ import com.kunzisoft.keepass.R 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.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper import com.kunzisoft.keepass.database.element.MainCredential import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -57,12 +55,6 @@ open class SettingsActivity private var toolbar: Toolbar? = null private var lockView: FloatingActionButton? = null - private var mHardwareKeyResponseHelper = HardwareKeyResponseHelper(this) - - override fun initializeDatabaseTaskProvider(): DatabaseTaskProvider { - return DatabaseTaskProvider(this, mHardwareKeyResponseHelper) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 259c8a4bd90bcba5ef76ec0333425486efa4f747 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 18 May 2022 16:39:35 +0200 Subject: [PATCH 013/171] Setting to remember hardware key --- .../2.json | 90 +++++++++++ .../activities/FileDatabaseSelectActivity.kt | 14 +- .../activities/MainCredentialActivity.kt | 152 +++++++++++++----- .../keepass/app/database/AppDatabase.kt | 9 +- .../app/database/FileDatabaseHistoryAction.kt | 51 +++--- .../app/database/FileDatabaseHistoryEntity.kt | 3 + .../database/action/CreateDatabaseRunnable.kt | 7 +- .../database/action/LoadDatabaseRunnable.kt | 7 +- .../kunzisoft/keepass/hardware/HardwareKey.kt | 8 +- .../kunzisoft/keepass/model/DatabaseFile.kt | 2 + .../keepass/settings/PreferencesUtil.kt | 6 + .../com/kunzisoft/keepass/utils/UriUtil.kt | 6 +- .../keepass/view/HardwareKeySelectionView.kt | 8 +- .../keepass/view/MainCredentialView.kt | 13 ++ .../viewmodels/DatabaseFilesViewModel.kt | 10 +- app/src/main/res/values/donottranslate.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../main/res/xml/preferences_application.xml | 9 +- 18 files changed, 319 insertions(+), 80 deletions(-) create mode 100644 app/schemas/com.kunzisoft.keepass.app.database.AppDatabase/2.json 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/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index e001841ed..0918ce702 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -56,6 +56,7 @@ import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation 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 @@ -155,8 +156,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> launchPasswordActivity( - databaseFileUri, - fileDatabaseHistoryEntityToOpen.keyFileUri + databaseFileUri, + fileDatabaseHistoryEntityToOpen.keyFileUri, + fileDatabaseHistoryEntityToOpen.hardwareKey ) } } @@ -250,7 +252,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(), ?: MainCredential() databaseFilesViewModel.addDatabaseFile( databaseUri, - mainCredential.keyFileUri + mainCredential.keyFileUri, + mainCredential.hardwareKey ) } } @@ -297,10 +300,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 +325,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/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt index 8edeaaf5b..2d6312e36 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt @@ -60,6 +60,7 @@ 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 @@ -102,6 +103,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 @@ -134,6 +137,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu PreferencesUtil.enableReadOnlyDatabase(this) } mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) + mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this) // Build elements to manage keyfile selection mExternalFileHelper = ExternalFileHelper(this) @@ -216,10 +220,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) } } @@ -227,6 +240,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)) { @@ -344,24 +358,36 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu private fun getUriFromIntent(intent: Intent?) { // If is a view intent val action = intent?.action - if (action != null - && action == VIEW_INTENT) { - mDatabaseFileUri = intent.data - mainCredentialView?.populateKeyFileView(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?.populateKeyFileView(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) @@ -370,7 +396,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() }, @@ -435,12 +461,19 @@ 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?.populateKeyFileView(keyFileUri) } + // Define hardware key + if (mRememberHardwareKey) { + mainCredentialView?.populateHardwareKeyView(hardwareKey) + } + // Define listener for validate button confirmButtonView?.setOnClickListener { mainCredentialView?.validateCredential() @@ -476,11 +509,15 @@ 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?.populateKeyFileView(null) } + if (clearHardwareKey) { + mainCredentialView?.populateHardwareKeyView(null) + } } override fun onPause() { @@ -670,18 +707,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) } @@ -694,8 +737,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) } } @@ -710,8 +754,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, @@ -729,8 +774,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, @@ -748,8 +794,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, @@ -768,10 +815,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, @@ -789,8 +837,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, @@ -806,6 +855,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu fun launch(activity: AppCompatActivity, databaseUri: Uri, keyFile: Uri?, + hardwareKey: HardwareKey?, fileNoFoundAction: (exception: FileNotFoundException) -> Unit, onCancelSpecialMode: () -> Unit, onLaunchActivitySpecialMode: () -> Unit, @@ -814,43 +864,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/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/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index bf3a53954..fdd1a568b 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 @@ -60,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)) mainCredential.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 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 1051cc541..a72ca3080 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 @@ -75,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context, // Save keyFile in app database if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context) - .addOrUpdateDatabaseUri(mDatabaseUri, - 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/hardware/HardwareKey.kt b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt index a8814454b..803cf96ca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt +++ b/app/src/main/java/com/kunzisoft/keepass/hardware/HardwareKey.kt @@ -4,6 +4,10 @@ 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 = FIDO2_SECRET @@ -19,7 +23,9 @@ enum class HardwareKey(val value: String) { } } - fun getHardwareKeyFromString(text: String): HardwareKey { + fun getHardwareKeyFromString(text: String?): HardwareKey? { + if (text == null) + return null values().find { it.value == text }?.let { return it } 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/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index 77fcdc552..f46f2c4e6 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), 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 0286b837e..e06d9da15 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -226,10 +226,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 +242,7 @@ object UriUtil { } } } catch (e: Exception) { - return intent.getParcelableExtra(key) + return intent?.getParcelableExtra(key) } return null } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt index f7d6bd063..e223751c9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/HardwareKeySelectionView.kt @@ -71,6 +71,11 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, 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) @@ -89,8 +94,7 @@ class HardwareKeySelectionView @JvmOverloads constructor(context: Context, } set(value) { mHardwareKey = value - if (value != null) - hardwareKeyCompletion.setSelection(value.ordinal) + hardwareKeyCompletion.setText(value?.toString() ?: "") } var error: CharSequence? 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 34a7f2786..325e18ba1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/MainCredentialView.kt @@ -40,6 +40,7 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.database.element.MainCredential +import com.kunzisoft.keepass.hardware.HardwareKey class MainCredentialView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @@ -156,6 +157,18 @@ 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) } 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/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 4ffdf4fbf..6f9d79050 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -91,6 +91,8 @@ true remember_keyfile_locations_key true + remember_hardware_key_key + true advanced_unlock_explanation_key biometric_unlock_enable_key false diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83ad082fe..7b97e435f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -292,6 +292,8 @@ Keeps track of where databases are stored Remember keyfile locations Keeps track of where keyfiles are stored + Remember hardware keys + Keeps track of the hardware keys used Show recent files Show locations of recent databases Hide broken database links diff --git a/app/src/main/res/xml/preferences_application.xml b/app/src/main/res/xml/preferences_application.xml index 31b4180bd..6daa9f62d 100644 --- a/app/src/main/res/xml/preferences_application.xml +++ b/app/src/main/res/xml/preferences_application.xml @@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . --> - + @@ -107,6 +106,12 @@ android:summary="@string/remember_keyfile_locations_summary" android:dependency="@string/remember_database_locations_key" android:defaultValue="@bool/remember_keyfile_locations_default"/> + Date: Wed, 18 May 2022 18:35:24 +0200 Subject: [PATCH 014/171] Better error management --- .../database/exception/DatabaseException.kt | 49 +++++++++++++++++-- .../DatabaseTaskNotificationService.kt | 13 +++-- app/src/main/res/values/strings.xml | 3 ++ 3 files changed, 56 insertions(+), 9 deletions(-) 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 f57d3a67c..f9ca05a3f 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,21 +24,60 @@ 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 +import java.lang.StringBuilder 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 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) } } 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 8e7427e13..e2217d3b2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -125,7 +125,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun addRequestChallengeListener(requestChallengeListener: RequestChallengeListener) { mainScope.launch { if (!mRequestChallengeListenerChannel.isEmpty) { - mRequestChallengeListenerChannel.cancel(CancellationException("Challenge already requested")) + mRequestChallengeListenerChannel.cancel(CancellationException(getString(R.string.error_challenge_already_requested))) mRequestChallengeListenerChannel = Channel(0) } else { mRequestChallengeListenerChannel.send(requestChallengeListener) @@ -234,11 +234,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun respondToChallenge(response: ByteArray) { mainScope.launch { - if (response.isEmpty()) { - mResponseChallengeChannel.cancel(CancellationException(("Unable to get the response from challenge"))) + if (!mResponseChallengeChannel.isEmpty) { + mResponseChallengeChannel.cancel(CancellationException(getString(R.string.error_response_already_provided))) mResponseChallengeChannel = Channel(0) } else { - mResponseChallengeChannel.send(response) + if (response.isEmpty()) { + mResponseChallengeChannel.cancel(CancellationException(getString(R.string.error_no_response_from_challenge))) + mResponseChallengeChannel = Channel(0) + } else { + mResponseChallengeChannel.send(response) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b97e435f..35e21584e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,6 +195,9 @@ The file data already exists. An error occurred while removing the file data. An error occurred while performing an action on the database. + "Challenge already requested" + Response already provided. + Unable to get the response from the challenge. Field name Field value Could not find file. Try reopening it from your file browser. From cbde96dd82f8d12c415a2bbdd80bc3aa882ccaff Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 18 May 2022 19:49:18 +0200 Subject: [PATCH 015/171] Add waiting task message and cancellable --- .../database/action/DatabaseTaskProvider.kt | 35 ++++++++++---- .../DatabaseTaskNotificationService.kt | 46 ++++++++++++++++--- .../tasks/ProgressTaskDialogFragment.kt | 19 ++++++-- app/src/main/res/layout/fragment_progress.xml | 10 ++++ app/src/main/res/values/strings.xml | 3 ++ 5 files changed, 93 insertions(+), 20 deletions(-) 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 c52d4d63d..1e3f88986 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 @@ -158,15 +158,25 @@ class DatabaseTaskProvider { } 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, + titleId: Int?, + messageId: Int?, + warningId: Int?, + cancelable: (() -> Unit)?) { + startDialog(titleId, messageId, warningId, cancelable) } - override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { - updateDialog(titleId, messageId, warningId) + override fun onUpdateAction(database: Database, + titleId: Int?, + messageId: Int?, + warningId: Int?, + cancelable: (() -> Unit)?) { + updateDialog(titleId, messageId, warningId, cancelable) } - 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() @@ -233,9 +243,10 @@ class DatabaseTaskProvider { } } - private fun startDialog(titleId: Int? = null, - messageId: Int? = null, - warningId: Int? = null) { + private fun startDialog(titleId: Int?, + messageId: Int?, + warningId: Int?, + cancelable: (() -> Unit)?) { activity?.let { activity -> activity.lifecycleScope.launch { if (progressTaskDialogFragment == null) { @@ -249,12 +260,15 @@ class DatabaseTaskProvider { PROGRESS_TASK_DIALOG_TAG ) } - updateDialog(titleId, messageId, warningId) + updateDialog(titleId, messageId, warningId, cancelable) } } } - private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) { + private fun updateDialog(titleId: Int?, + messageId: Int?, + warningId: Int?, + cancelable: (() -> Unit)?) { progressTaskDialogFragment?.apply { titleId?.let { updateTitle(it) @@ -265,6 +279,7 @@ class DatabaseTaskProvider { warningId?.let { updateWarning(it) } + setCancellable(cancelable) } } 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 e2217d3b2..099367c6c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -81,7 +81,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private var mTitleId: Int = R.string.database_opened private var mMessageId: Int? = null private var mWarningId: Int? = null - + private var mCancelable: (() -> Unit)? = null override fun retrieveChannelId(): String { return CHANNEL_DATABASE_ID @@ -151,9 +151,19 @@ 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, + titleId: Int?, + messageId: Int?, + warningId: Int?, + cancelable: (() -> Unit)? = null) + fun onUpdateAction(database: Database, + titleId: Int?, + messageId: Int?, + warningId: Int?, + cancelable: (() -> Unit)? = null) + fun onStopAction(database: Database, + actionTask: String, + result: ActionRunnable.Result) } interface RequestChallengeListener { @@ -226,7 +236,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress mDatabase?.let { database -> if (mActionRunning) { mActionTaskListeners.forEach { actionTaskListener -> - actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId) + actionTaskListener.onStartAction( + database, mTitleId, mMessageId, mWarningId, mCancelable + ) } } } @@ -332,7 +344,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress }) mActionTaskListeners.forEach { actionTaskListener -> - actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId) + actionTaskListener.onStartAction( + database, mTitleId, mMessageId, mWarningId, mCancelable + ) } }, @@ -575,7 +589,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress mMessageId = resId mDatabase?.let { database -> mActionTaskListeners.forEach { actionTaskListener -> - actionTaskListener.onUpdateAction(database, mTitleId, mMessageId, mWarningId) + actionTaskListener.onUpdateAction( + database, mTitleId, mMessageId, mWarningId, mCancelable + ) } } } @@ -603,10 +619,26 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress var response: ByteArray runBlocking { // Send the request + val previousMessageId = mMessageId + mCancelable = { + mRequestChallengeListenerChannel.cancel(CancellationException(getString(R.string.error_cancel_by_user))) + mRequestChallengeListenerChannel = Channel(0) + } + updateMessage(R.string.waiting_challenge_request) val challengeResponseRequestListener = mRequestChallengeListenerChannel.receive() challengeResponseRequestListener.onChallengeResponseRequested(hardwareKey, seed) // Wait the response + mCancelable = { + mResponseChallengeChannel.cancel(CancellationException(getString(R.string.error_cancel_by_user))) + mResponseChallengeChannel = Channel(0) + } + updateMessage(R.string.waiting_challenge_response) response = mResponseChallengeChannel.receive() ?: byteArrayOf() + // Restore previous message + mCancelable = null + previousMessageId?.let { + updateMessage(it) + } } return response } 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..91f037367 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,16 @@ 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 com.kunzisoft.keepass.R -import java.lang.Exception -open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { +open class ProgressTaskDialogFragment : DialogFragment() { @StringRes private var title = UNDEFINED @@ -40,10 +41,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 +66,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 @@ -99,7 +104,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { updateView(titleView, title) } - override fun updateMessage(@StringRes resId: Int) { + fun updateMessage(@StringRes resId: Int) { this.message = resId updateView(messageView, message) } @@ -109,6 +114,14 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { updateView(warningView, warning) } + fun setCancellable(cancellable: (() -> Unit)?) { + this.cancellable = cancellable + cancelButton?.isVisible = cancellable != null + cancelButton?.setOnClickListener { + cancellable?.invoke() + } + } + companion object { private val TAG = ProgressTaskDialogFragment::class.java.simpleName const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment" 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"/> +