diff --git a/CHANGELOG b/CHANGELOG
index 9c4373565..31b354f05 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,6 @@
+KeePassDX(3.2.0)
+ * Manage data merge #840 #977
+
KeePassDX(3.1.0)
* Add breadcrumb
* Add path in search results #1148
diff --git a/app/build.gradle b/app/build.gradle
index 5fa9ea69a..2dce2f675 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 31
- versionCode = 92
- versionName = "3.1.0"
+ versionCode = 93
+ versionName = "3.2.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
index e8cf1f74b..2bbb71dba 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
@@ -376,6 +376,9 @@ class EntryActivity : DatabaseLockActivity() {
menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.isVisible = false
}
+ if (!mMergeDataAllowed || mDatabaseReadOnly) {
+ menu?.findItem(R.id.menu_merge_database)?.isVisible = false
+ }
if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
}
@@ -455,6 +458,9 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_save_database -> {
saveDatabase()
}
+ R.id.menu_merge_database -> {
+ mergeDatabase()
+ }
R.id.menu_reload_database -> {
reloadDatabase()
}
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 5b8455680..43f8e9f9c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -970,6 +970,9 @@ class GroupActivity : DatabaseLockActivity(),
if (mDatabaseReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
}
+ if (!mMergeDataAllowed || mDatabaseReadOnly) {
+ menu.findItem(R.id.menu_merge_database)?.isVisible = false
+ }
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu)
} else {
@@ -1093,6 +1096,10 @@ class GroupActivity : DatabaseLockActivity(),
saveDatabase()
return true
}
+ R.id.menu_merge_database -> {
+ mergeDatabase()
+ return true
+ }
R.id.menu_reload_database -> {
reloadDatabase()
return true
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 f5de6dd2e..6b5e79d64 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
@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
private var mExitLock: Boolean = false
protected var mDatabaseReadOnly: Boolean = true
+ protected var mMergeDataAllowed: Boolean = false
private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null
@@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save)
}
+ mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid ->
+ mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid)
+ }
+
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
- mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
+ mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
+ mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
+ }
}
mDatabaseViewModel.saveName.observe(this) {
@@ -197,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
mDatabaseReadOnly = database.isReadOnly
+ mMergeDataAllowed = database.isMergeDataAllowed()
mIconDrawableFactory = database.iconDrawableFactory
checkRegister()
@@ -212,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
+ DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
if (result.isSuccess) {
@@ -254,8 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(true)
}
+ fun mergeDatabase() {
+ mDatabaseTaskProvider?.startDatabaseMerge(false)
+ }
+
fun reloadDatabase() {
- mDatabaseTaskProvider?.startDatabaseReload(false)
+ mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
+ mDatabaseTaskProvider?.startDatabaseReload(false)
+ }
}
fun createEntry(newEntry: Entry,
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt
index f5d121d73..9ae3beef7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
+import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
@@ -112,7 +113,7 @@ class SearchEntryCursorAdapter(private val context: Context,
database.getStandardIcon(standardIconId)
},
{ customIconId ->
- database.getCustomIcon(customIconId)
+ database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId)
}
)
}
@@ -122,7 +123,7 @@ class SearchEntryCursorAdapter(private val context: Context,
database.getStandardIcon(standardIconId)
},
{ customIconId ->
- database.getCustomIcon(customIconId)
+ database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId)
}
)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
index 3bb1ed0f7..a49096425 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt
@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
- database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
+ database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) {
erase(mBackupKey)
setError(e)
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 20848b4a5..c98a44eeb 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
@@ -27,6 +27,7 @@ import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
@@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
+import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
@@ -354,6 +356,13 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_LOAD_TASK)
}
+ fun startDatabaseMerge(fixDuplicateUuid: Boolean) {
+ start(Bundle().apply {
+ putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
+ }
+ , ACTION_DATABASE_MERGE_TASK)
+ }
+
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -361,6 +370,19 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_RELOAD_TASK)
}
+ fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
+ if (conditionToAsk) {
+ AlertDialog.Builder(context)
+ .setMessage(R.string.warning_database_info_reloaded)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ approved.invoke()
+ }.create().show()
+ } else {
+ approved.invoke()
+ }
+ }
+
fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) {
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 a5b2408ca..ef1c2be70 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
@@ -60,7 +60,6 @@ class LoadDatabaseRunnable(private val context: Context,
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
- LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID,
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
new file mode 100644
index 000000000..1a6a09c95
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 Jeremy Jamet / Kunzisoft.
+ *
+ * This file is part of KeePassDX.
+ *
+ * KeePassDX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KeePassDX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with KeePassDX. If not, see .
+ *
+ */
+package com.kunzisoft.keepass.database.action
+
+import android.content.Context
+import com.kunzisoft.keepass.database.element.Database
+import com.kunzisoft.keepass.database.element.binary.BinaryData
+import com.kunzisoft.keepass.database.element.binary.LoadedKey
+import com.kunzisoft.keepass.database.exception.LoadDatabaseException
+import com.kunzisoft.keepass.settings.PreferencesUtil
+import com.kunzisoft.keepass.tasks.ActionRunnable
+import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
+import com.kunzisoft.keepass.utils.UriUtil
+
+class MergeDatabaseRunnable(private val context: Context,
+ private val mDatabase: Database,
+ private val progressTaskUpdater: ProgressTaskUpdater?,
+ private val mLoadDatabaseResult: ((Result) -> Unit)?)
+ : ActionRunnable() {
+
+ override fun onStartRun() {
+ mDatabase.wasReloaded = true
+ }
+
+ override fun onActionRun() {
+ try {
+ mDatabase.mergeData(context.contentResolver,
+ { memoryWanted ->
+ BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
+ },
+ progressTaskUpdater)
+ } catch (e: LoadDatabaseException) {
+ setError(e)
+ }
+
+ if (result.isSuccess) {
+ // Register the current time to init the lock timer
+ PreferencesUtil.saveCurrentTime(context)
+ }
+ }
+
+ override fun onFinishRun() {
+ mLoadDatabaseResult?.invoke(result)
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt
index 7ee45188c..231c63d87 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
-import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
- private var tempCipherKey: LoadedKey? = null
-
override fun onStartRun() {
- tempCipherKey = mDatabase.binaryCache.loadedCipherKey
// Clear before we load
- mDatabase.clear(UriUtil.getBinaryDir(context))
+ mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
mDatabase.wasReloaded = true
}
override fun onActionRun() {
try {
mDatabase.reloadData(context.contentResolver,
- UriUtil.getBinaryDir(context),
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
- tempCipherKey ?: LoadedKey.generateNewCipherKey(),
progressTaskUpdater)
} catch (e: LoadDatabaseException) {
setError(e)
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
- tempCipherKey = null
mDatabase.clearAndClose(context)
}
}
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 1816abd60..2175a6b08 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
@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
foreachNode@ for(nodeToDelete in mNodesToDelete) {
mOldParent = nodeToDelete.parent
- mOldParent?.touch(modified = false, touchParents = true)
+ nodeToDelete.touch(modified = true, touchParents = true)
when (nodeToDelete.type) {
Type.GROUP -> {
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent
mCanRecycle = database.canRecycle(groupToDelete)
if (mCanRecycle) {
- groupToDelete.touch(modified = false, touchParents = true)
database.recycle(groupToDelete, context.resources)
groupToDelete.setPreviousParentGroup(mOldParent)
+ groupToDelete.touch(modified = true, touchParents = true)
} else {
database.deleteGroup(groupToDelete)
}
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent
mCanRecycle = database.canRecycle(entryToDelete)
if (mCanRecycle) {
- entryToDelete.touch(modified = false, touchParents = true)
database.recycle(entryToDelete, context.resources)
entryToDelete.setPreviousParentGroup(mOldParent)
+ entryToDelete.touch(modified = true, touchParents = true)
} else {
database.deleteEntry(entryToDelete)
}
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 d93a0af93..17ad24fa3 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
@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
foreachNode@ for(nodeToMove in mNodesToMove) {
// Move node in new parent
mOldParent = nodeToMove.parent
+ nodeToMove.touch(modified = true, touchParents = true)
when (nodeToMove.type) {
Type.GROUP -> {
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
// and if not in the current group
&& groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) {
- groupToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent)
groupToMove.setPreviousParentGroup(mOldParent)
+ groupToMove.touch(modified = true, touchParents = true)
} else {
// Only finish thread
setError(MoveGroupDatabaseException())
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
if (mOldParent != mNewParent
// and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
- entryToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent)
entryToMove.setPreviousParentGroup(mOldParent)
+ entryToMove.touch(modified = true, touchParents = true)
} else {
// Only finish thread
setError(MoveEntryDatabaseException())
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 5e5c6def2..b3732bacc 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
@@ -32,7 +32,6 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData
-import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
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.icons.IconDrawableFactory
@@ -94,6 +94,8 @@ class Database {
*/
var wasReloaded = false
+ var dataModifiedSinceLastLoading = false
+
var loadTimestamp: Long? = null
private set
@@ -112,7 +114,7 @@ class Database {
private val iconsManager: IconsManager
get() {
- return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
+ return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
}
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
@@ -130,7 +132,7 @@ class Database {
return iconsManager.doForEachCustomIcon(action)
}
- fun getCustomIcon(iconId: UUID): IconImageCustom {
+ fun getCustomIcon(iconId: UUID): IconImageCustom? {
return iconsManager.getIcon(iconId)
}
@@ -144,11 +146,12 @@ class Database {
fun removeCustomIcon(customIcon: IconImageCustom) {
iconDrawableFactory.clearFromCache(customIcon)
- iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
+ iconsManager.removeCustomIcon(customIcon.uuid, binaryCache)
+ mDatabaseKDBX?.addDeletedObject(customIcon.uuid)
}
fun updateCustomIcon(customIcon: IconImageCustom) {
- iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
+ iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon)
}
fun getTemplates(templateCreation: Boolean): List {
@@ -212,6 +215,7 @@ class Database {
set(name) {
mDatabaseKDBX?.name = name
mDatabaseKDBX?.nameChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
val allowDescription: Boolean
@@ -224,6 +228,7 @@ class Database {
set(description) {
mDatabaseKDBX?.description = description
mDatabaseKDBX?.descriptionChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var defaultUsername: String
@@ -234,6 +239,7 @@ class Database {
mDatabaseKDB?.defaultUserName = username
mDatabaseKDBX?.defaultUserName = username
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var customColor: Int?
@@ -253,6 +259,8 @@ class Database {
} else {
ChromaUtil.getFormattedColorString(value, false)
}
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
val allowOTP: Boolean
@@ -276,6 +284,8 @@ class Database {
value?.let {
mDatabaseKDBX?.compressionAlgorithm = it
}
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
fun compressionForNewEntry(): Boolean {
@@ -292,6 +302,7 @@ class Database {
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
+ dataModifiedSinceLastLoading = true
}
val allowNoMasterKey: Boolean
@@ -306,14 +317,12 @@ class Database {
val availableEncryptionAlgorithms: List
get() = mDatabaseKDB?.availableEncryptionAlgorithms ?: mDatabaseKDBX?.availableEncryptionAlgorithms ?: ArrayList()
- var encryptionAlgorithm: EncryptionAlgorithm?
- get() = mDatabaseKDB?.encryptionAlgorithm ?: mDatabaseKDBX?.encryptionAlgorithm
- set(algorithm) {
- algorithm?.let {
- mDatabaseKDBX?.encryptionAlgorithm = algorithm
- mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
- mDatabaseKDBX?.cipherUuid = algorithm.uuid
- }
+ var encryptionAlgorithm: EncryptionAlgorithm
+ get() = mDatabaseKDB?.encryptionAlgorithm
+ ?: mDatabaseKDBX?.encryptionAlgorithm
+ ?: EncryptionAlgorithm.AESRijndael
+ set(value) {
+ mDatabaseKDBX?.encryptionAlgorithm = value
}
val availableKdfEngines: List
@@ -343,6 +352,8 @@ class Database {
set(numberRounds) {
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var memoryUsage: Long
@@ -351,12 +362,16 @@ class Database {
}
set(memory) {
mDatabaseKDBX?.memoryUsage = memory
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var parallelism: Long
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
set(parallelism) {
mDatabaseKDBX?.parallelism = parallelism
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var masterKey: ByteArray
@@ -364,6 +379,8 @@ class Database {
set(masterKey) {
mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var rootGroup: Group?
@@ -414,6 +431,8 @@ class Database {
}
set(value) {
mDatabaseKDBX?.historyMaxItems = value
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
var historyMaxSize: Long
@@ -422,6 +441,8 @@ class Database {
}
set(value) {
mDatabaseKDBX?.historyMaxSize = value
+ mDatabaseKDBX?.settingsChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
/**
@@ -442,15 +463,17 @@ class Database {
} else {
mDatabaseKDBX?.removeRecycleBin()
}
+ mDatabaseKDBX?.recycleBinChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
val recycleBin: Group?
get() {
mDatabaseKDB?.backupGroup?.let {
- return Group(it)
+ return getGroupById(it.nodeId) ?: Group(it)
}
mDatabaseKDBX?.recycleBin?.let {
- return Group(it)
+ return getGroupById(it.nodeId) ?: Group(it)
}
return null
}
@@ -460,8 +483,10 @@ class Database {
if (group != null) {
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
} else {
- mDatabaseKDBX?.removeTemplatesGroup()
+ mDatabaseKDBX?.removeRecycleBin()
}
+ mDatabaseKDBX?.recycleBinChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
/**
@@ -477,6 +502,8 @@ class Database {
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
+ mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
val templatesGroup: Group?
@@ -492,8 +519,10 @@ class Database {
if (group != null) {
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
} else {
- mDatabaseKDBX?.entryTemplatesGroup
+ mDatabaseKDBX?.removeTemplatesGroup()
}
+ mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
+ dataModifiedSinceLastLoading = true
}
val groupNamesNotAllowed: List
@@ -520,6 +549,7 @@ class Database {
this.fileUri = databaseUri
// Set Database state
this.loaded = true
+ this.dataModifiedSinceLastLoading = false
}
@Throws(LoadDatabaseException::class)
@@ -576,7 +606,6 @@ class Database {
contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
- tempCipherKey: LoadedKey,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@@ -597,73 +626,156 @@ class Database {
// Read database stream for the first time
readDatabaseStream(contentResolver, uri,
{ databaseInputStream ->
- DatabaseInputKDB(cacheDirectory, isRAMSufficient)
- .openDatabase(databaseInputStream,
- mainCredential.masterPassword,
- keyFileInputStream,
- tempCipherKey,
- progressTaskUpdater,
- fixDuplicateUUID)
+ val databaseKDB = DatabaseKDB().apply {
+ binaryCache.cacheDirectory = cacheDirectory
+ changeDuplicateId = fixDuplicateUUID
+ }
+ DatabaseInputKDB(databaseKDB)
+ .openDatabase(databaseInputStream,
+ mainCredential.masterPassword,
+ keyFileInputStream,
+ progressTaskUpdater)
+ databaseKDB
},
{ databaseInputStream ->
- DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
- .openDatabase(databaseInputStream,
- mainCredential.masterPassword,
- keyFileInputStream,
- tempCipherKey,
- progressTaskUpdater,
- fixDuplicateUUID)
+ val databaseKDBX = DatabaseKDBX().apply {
+ binaryCache.cacheDirectory = cacheDirectory
+ changeDuplicateId = fixDuplicateUUID
+ }
+ DatabaseInputKDBX(databaseKDBX).apply {
+ setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
+ openDatabase(databaseInputStream,
+ mainCredential.masterPassword,
+ keyFileInputStream,
+ progressTaskUpdater)
+ }
+ databaseKDBX
}
)
} catch (e: FileNotFoundException) {
- Log.e(TAG, "Unable to load keyfile", e)
- throw FileNotFoundDatabaseException()
+ throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
keyFileInputStream?.close()
+ dataModifiedSinceLastLoading = false
+ }
+ }
+
+ fun isMergeDataAllowed(): Boolean {
+ return mDatabaseKDBX != null
+ }
+
+ @Throws(LoadDatabaseException::class)
+ fun mergeData(contentResolver: ContentResolver,
+ isRAMSufficient: (memoryWanted: Long) -> Boolean,
+ progressTaskUpdater: ProgressTaskUpdater?) {
+
+ mDatabaseKDB?.let {
+ throw IODatabaseException("Unable to merge from a database V1")
+ }
+
+ // New database instance to get new changes
+ val databaseToMerge = Database()
+ databaseToMerge.fileUri = this.fileUri
+
+ try {
+ databaseToMerge.fileUri?.let { databaseUri ->
+
+ val databaseKDB = DatabaseKDB()
+ val databaseKDBX = DatabaseKDBX()
+
+ databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
+ { databaseInputStream ->
+ DatabaseInputKDB(databaseKDB)
+ .openDatabase(databaseInputStream,
+ masterKey,
+ progressTaskUpdater)
+ databaseKDB
+ },
+ { databaseInputStream ->
+ DatabaseInputKDBX(databaseKDBX).apply {
+ setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
+ openDatabase(databaseInputStream,
+ masterKey,
+ progressTaskUpdater)
+ }
+ databaseKDBX
+ }
+ )
+
+ mDatabaseKDBX?.let { currentDatabaseKDBX ->
+ val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
+ this.isRAMSufficient = isRAMSufficient
+ }
+ databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge ->
+ databaseMerger.merge(databaseKDBToMerge)
+ }
+ databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge ->
+ databaseMerger.merge(databaseKDBXToMerge)
+ }
+ }
+ } ?: run {
+ 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) {
+ throw LoadDatabaseException(e)
+ } finally {
+ databaseToMerge.clearAndClose()
}
}
@Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver,
- cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
- tempCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI
try {
fileUri?.let { oldDatabaseUri ->
readDatabaseStream(contentResolver, oldDatabaseUri,
- { databaseInputStream ->
- DatabaseInputKDB(cacheDirectory, isRAMSufficient)
- .openDatabase(databaseInputStream,
- masterKey,
- tempCipherKey,
- progressTaskUpdater)
- },
- { databaseInputStream ->
- DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
- .openDatabase(databaseInputStream,
- masterKey,
- tempCipherKey,
- progressTaskUpdater)
+ { databaseInputStream ->
+ val databaseKDB = DatabaseKDB()
+ mDatabaseKDB?.let {
+ databaseKDB.binaryCache = it.binaryCache
}
+ DatabaseInputKDB(databaseKDB)
+ .openDatabase(databaseInputStream,
+ masterKey,
+ progressTaskUpdater)
+ databaseKDB
+ },
+ { databaseInputStream ->
+ val databaseKDBX = DatabaseKDBX()
+ mDatabaseKDBX?.let {
+ databaseKDBX.binaryCache = it.binaryCache
+ }
+ DatabaseInputKDBX(databaseKDBX).apply {
+ setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
+ openDatabase(databaseInputStream,
+ masterKey,
+ progressTaskUpdater)
+ }
+ databaseKDBX
+ }
)
} ?: run {
- Log.e(TAG, "Database URI is null, database cannot be reloaded")
- throw IODatabaseException()
+ throw IODatabaseException("Database URI is null, database cannot be reloaded")
}
} catch (e: FileNotFoundException) {
- Log.e(TAG, "Unable to load keyfile", e)
- throw FileNotFoundDatabaseException()
+ throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
+ } finally {
+ dataModifiedSinceLastLoading = false
}
}
@@ -703,7 +815,7 @@ class Database {
val attachmentPool: AttachmentPool
get() {
- return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
+ return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool()
}
val allowMultipleAttachments: Boolean
@@ -716,8 +828,8 @@ class Database {
}
fun buildNewBinaryAttachment(): BinaryData? {
- return mDatabaseKDB?.buildNewAttachment()
- ?: mDatabaseKDBX?.buildNewAttachment( false,
+ return mDatabaseKDB?.buildNewBinaryAttachment()
+ ?: mDatabaseKDBX?.buildNewBinaryAttachment( false,
compressionForNewEntry(),
false)
}
@@ -731,6 +843,7 @@ class Database {
fun removeUnlinkedAttachments() {
// No check in database KDB because unique attachment by entry
mDatabaseKDBX?.removeUnlinkedAttachments(true)
+ dataModifiedSinceLastLoading = true
}
@Throws(DatabaseOutputException::class)
@@ -791,16 +904,25 @@ class Database {
}
}
this.fileUri = uri
+ this.dataModifiedSinceLastLoading = false
}
- fun clear(filesDirectory: File? = null) {
- binaryCache.clear()
- iconsManager.clearCache()
+ fun clearIndexesAndBinaries(filesDirectory: File? = null) {
+ this.mDatabaseKDB?.clearIndexes()
+ this.mDatabaseKDBX?.clearIndexes()
+
+ this.mDatabaseKDB?.clearIconsCache()
+ this.mDatabaseKDBX?.clearIconsCache()
+
+ this.mDatabaseKDB?.clearAttachmentsCache()
+ this.mDatabaseKDBX?.clearAttachmentsCache()
+
+ this.mDatabaseKDB?.clearBinaries()
+ this.mDatabaseKDBX?.clearBinaries()
+
iconDrawableFactory.clearCache()
- // Delete the cache of the database if present
- mDatabaseKDB?.clearCache()
- mDatabaseKDBX?.clearCache()
- // In all cases, delete all the files in the temp dir
+
+ // delete all the files in the temp dir if allowed
try {
filesDirectory?.let { directory ->
cleanDirectory(directory)
@@ -811,7 +933,7 @@ class Database {
}
fun clearAndClose(context: Context? = null) {
- clear(context?.let { UriUtil.getBinaryDir(context) })
+ clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) })
this.mDatabaseKDB = null
this.mDatabaseKDBX = null
this.fileUri = null
@@ -838,9 +960,10 @@ class Database {
}
@Throws(IOException::class)
- fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
+ fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
+ mDatabaseKDBX?.keyLastChanged = DateInstant()
}
fun rootCanContainsEntry(): Boolean {
@@ -848,6 +971,7 @@ class Database {
}
fun createEntry(): Entry? {
+ dataModifiedSinceLastLoading = true
mDatabaseKDB?.let { database ->
return Entry(database.createEntry()).apply {
nodeId = database.newEntryId()
@@ -863,6 +987,7 @@ class Database {
}
fun createGroup(): Group? {
+ dataModifiedSinceLastLoading = true
mDatabaseKDB?.let { database ->
return Group(database.createGroup()).apply {
setNodeId(database.newGroupId())
@@ -900,6 +1025,7 @@ class Database {
}
fun addEntryTo(entry: Entry, parent: Group) {
+ dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
}
@@ -910,6 +1036,7 @@ class Database {
}
fun updateEntry(entry: Entry) {
+ dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.updateEntry(entryKDB)
}
@@ -919,6 +1046,7 @@ class Database {
}
fun removeEntryFrom(entry: Entry, parent: Group) {
+ dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
}
@@ -929,6 +1057,7 @@ class Database {
}
fun addGroupTo(group: Group, parent: Group) {
+ dataModifiedSinceLastLoading = true
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
}
@@ -939,6 +1068,7 @@ class Database {
}
fun updateGroup(group: Group) {
+ dataModifiedSinceLastLoading = true
group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.updateGroup(entryKDB)
}
@@ -948,6 +1078,7 @@ class Database {
}
fun removeGroupFrom(group: Group, parent: Group) {
+ dataModifiedSinceLastLoading = true
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
}
@@ -986,12 +1117,17 @@ class Database {
}
fun deleteEntry(entry: Entry) {
+ dataModifiedSinceLastLoading = true
+ entry.entryKDBX?.id?.let { entryId ->
+ mDatabaseKDBX?.addDeletedObject(entryId)
+ }
entry.parent?.let {
removeEntryFrom(entry, it)
}
}
fun deleteGroup(group: Group) {
+ dataModifiedSinceLastLoading = true
group.doForEachChildAndForIt(
object : NodeHandler() {
override fun operate(node: Entry): Boolean {
@@ -1001,6 +1137,9 @@ class Database {
},
object : NodeHandler() {
override fun operate(node: Group): Boolean {
+ node.groupKDBX?.id?.let { groupId ->
+ mDatabaseKDBX?.addDeletedObject(groupId)
+ }
node.parent?.let {
removeGroupFrom(node, it)
}
@@ -1009,24 +1148,6 @@ class Database {
})
}
- fun undoDeleteEntry(entry: Entry, parent: Group) {
- entry.entryKDB?.let {
- mDatabaseKDB?.undoDeleteEntryFrom(it, parent.groupKDB)
- }
- entry.entryKDBX?.let {
- mDatabaseKDBX?.undoDeleteEntryFrom(it, parent.groupKDBX)
- }
- }
-
- fun undoDeleteGroup(group: Group, parent: Group) {
- group.groupKDB?.let {
- mDatabaseKDB?.undoDeleteGroupFrom(it, parent.groupKDB)
- }
- group.groupKDBX?.let {
- mDatabaseKDBX?.undoDeleteGroupFrom(it, parent.groupKDBX)
- }
- }
-
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
@@ -1055,47 +1176,41 @@ class Database {
}
fun recycle(entry: Entry, resources: Resources) {
- entry.entryKDB?.let {
- mDatabaseKDB?.recycle(it)
+ ensureRecycleBinExists(resources)
+ entry.parent?.let { parent ->
+ removeEntryFrom(entry, parent)
}
- entry.entryKDBX?.let {
- mDatabaseKDBX?.recycle(it, resources)
+ recycleBin?.let {
+ addEntryTo(entry, it)
}
+ entry.afterAssignNewParent()
}
fun recycle(group: Group, resources: Resources) {
- group.groupKDB?.let {
- mDatabaseKDB?.recycle(it)
+ ensureRecycleBinExists(resources)
+ group.parent?.let { parent ->
+ removeGroupFrom(group, parent)
}
- group.groupKDBX?.let {
- mDatabaseKDBX?.recycle(it, resources)
+ recycleBin?.let {
+ addGroupTo(group, it)
}
+ group.afterAssignNewParent()
}
fun undoRecycle(entry: Entry, parent: Group) {
- entry.entryKDB?.let { entryKDB ->
- parent.groupKDB?.let { parentKDB ->
- mDatabaseKDB?.undoRecycle(entryKDB, parentKDB)
- }
- }
- entry.entryKDBX?.let { entryKDBX ->
- parent.groupKDBX?.let { parentKDBX ->
- mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
- }
+ recycleBin?.let { it ->
+ removeEntryFrom(entry, it)
}
+ addEntryTo(entry, parent)
+ entry.afterAssignNewParent()
}
fun undoRecycle(group: Group, parent: Group) {
- group.groupKDB?.let { groupKDB ->
- parent.groupKDB?.let { parentKDB ->
- mDatabaseKDB?.undoRecycle(groupKDB, parentKDB)
- }
- }
- group.groupKDBX?.let { entryKDBX ->
- parent.groupKDBX?.let { parentKDBX ->
- mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
- }
+ recycleBin?.let {
+ removeGroupFrom(group, it)
}
+ addGroupTo(group, parent)
+ group.afterAssignNewParent()
}
fun startManageEntry(entry: Entry?) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt
index 7306dde70..2c5ab1292 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/DeletedObject.kt
@@ -28,29 +28,18 @@ import java.util.*
class DeletedObject : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
- private var mDeletionTime: DateInstant? = null
+ var deletionTime: DateInstant = DateInstant()
constructor()
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
this.uuid = uuid
- this.mDeletionTime = deletionTime
+ this.deletionTime = deletionTime
}
constructor(parcel: Parcel) {
uuid = parcel.readParcelable(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
- mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
- }
-
- fun getDeletionTime(): DateInstant {
- if (mDeletionTime == null) {
- mDeletionTime = DateInstant(System.currentTimeMillis())
- }
- return mDeletionTime!!
- }
-
- fun setDeletionTime(deletionTime: DateInstant) {
- this.mDeletionTime = deletionTime
+ deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
}
override fun equals(other: Any?): Boolean {
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ParcelUuid(uuid), flags)
- parcel.writeParcelable(mDeletionTime, flags)
+ parcel.writeParcelable(deletionTime, flags)
}
override fun describeContents(): Int {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/AttachmentPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/AttachmentPool.kt
index db9a9c7e6..ed96f1dd8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/AttachmentPool.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/AttachmentPool.kt
@@ -19,7 +19,7 @@
*/
package com.kunzisoft.keepass.database.element.binary
-class AttachmentPool(binaryCache: BinaryCache) : BinaryPool(binaryCache) {
+class AttachmentPool : BinaryPool() {
/**
* Utility method to find an unused key in the pool
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/BinaryPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/BinaryPool.kt
index b18728477..ba639599a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/BinaryPool.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/BinaryPool.kt
@@ -23,7 +23,7 @@ import android.util.Log
import java.io.IOException
import kotlin.math.abs
-abstract class BinaryPool(private val mBinaryCache: BinaryCache) {
+abstract class BinaryPool {
protected val pool = LinkedHashMap()
@@ -225,9 +225,6 @@ abstract class BinaryPool(private val mBinaryCache: BinaryCache) {
@Throws(IOException::class)
fun clear() {
- doForEachBinary { _, binary ->
- binary.clear(mBinaryCache)
- }
pool.clear()
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt
index f2789c39a..7b5ae7892 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/binary/CustomIconPool.kt
@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import java.util.*
-class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool(binaryCache) {
+class CustomIconPool : BinaryPool() {
private val customIcons = HashMap()
fun put(key: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
- smallSize: Boolean,
+ builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) {
- val keyBinary = super.put(key) { uniqueBinaryId ->
- // Create a byte array for better performance with small data
- binaryCache.getBinaryData(uniqueBinaryId, smallSize)
- }
+ val keyBinary = super.put(key, builder)
val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
customIcons[uuid] = customIcon
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 2bd047f8b..8d267895a 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
@@ -34,11 +34,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import java.io.IOException
import java.io.InputStream
import java.util.*
-import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned() {
- private var kdfListV3: MutableList = ArrayList()
+ override var encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
+
+ override val availableEncryptionAlgorithms: List = listOf(
+ EncryptionAlgorithm.AESRijndael,
+ EncryptionAlgorithm.Twofish
+ )
+
+ override val kdfEngine: KdfEngine
+ get() = kdfAvailableList[0]
+
+ override val kdfAvailableList: List = listOf(
+ KdfFactory.aesKdf
+ )
+
+ override val passwordEncoding: String
+ get() = "ISO-8859-1"
+
+ override var numberKeyEncryptionRounds = 300L
override val version: String
get() = "V1"
@@ -48,7 +64,6 @@ class DatabaseKDB : DatabaseVersioned() {
rootGroup = createGroup().apply {
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
}
- kdfListV3.add(KdfFactory.aesKdf)
}
val backupGroup: GroupKDB?
@@ -65,28 +80,6 @@ class DatabaseKDB : DatabaseVersioned() {
var color: Int? = null
- override val kdfEngine: KdfEngine
- get() = kdfListV3[0]
-
- override val kdfAvailableList: List
- get() = kdfListV3
-
- override val availableEncryptionAlgorithms: List
- get() {
- val list = ArrayList()
- list.add(EncryptionAlgorithm.AESRijndael)
- list.add(EncryptionAlgorithm.Twofish)
- return list
- }
-
- override val passwordEncoding: String
- get() = "ISO-8859-1"
-
- override var numberKeyEncryptionRounds = 300L
-
- init {
- algorithm = EncryptionAlgorithm.AESRijndael
- }
/**
* Generates an unused random tree id
@@ -212,29 +205,7 @@ class DatabaseKDB : DatabaseVersioned() {
return true
}
- fun recycle(group: GroupKDB) {
- removeGroupFrom(group, group.parent)
- addGroupTo(group, backupGroup)
- group.afterAssignNewParent()
- }
-
- fun recycle(entry: EntryKDB) {
- removeEntryFrom(entry, entry.parent)
- addEntryTo(entry, backupGroup)
- entry.afterAssignNewParent()
- }
-
- fun undoRecycle(group: GroupKDB, origParent: GroupKDB) {
- removeGroupFrom(group, backupGroup)
- addGroupTo(group, origParent)
- }
-
- fun undoRecycle(entry: EntryKDB, origParent: GroupKDB) {
- removeEntryFrom(entry, backupGroup)
- addEntryTo(entry, origParent)
- }
-
- fun buildNewAttachment(): BinaryData {
+ fun buildNewBinaryAttachment(): BinaryData {
// Generate an unique new file
return attachmentPool.put { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, false)
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 427532a0a..e47dfe781 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
@@ -25,8 +25,6 @@ import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.action.node.NodeHandler
-import com.kunzisoft.keepass.database.crypto.AesEngine
-import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
@@ -42,12 +40,12 @@ import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
+import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
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.UnknownKDF
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
@@ -66,6 +64,7 @@ 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
@@ -73,27 +72,58 @@ class DatabaseKDBX : DatabaseVersioned {
var hmacKey: ByteArray? = null
private set
- var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
- private var dataEngine: CipherEngine = AesEngine()
- var compressionAlgorithm = CompressionAlgorithm.GZip
+
+ override var encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
+
+ fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
+ encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
+ }
+
+ override val availableEncryptionAlgorithms: List = listOf(
+ EncryptionAlgorithm.AESRijndael,
+ EncryptionAlgorithm.Twofish,
+ EncryptionAlgorithm.ChaCha20
+ )
+
+ override val kdfEngine: KdfEngine?
+ get() {
+ val keyDerivationFunctionParameters = kdfParameters ?: return null
+ for (engine in kdfAvailableList) {
+ if (engine.uuid == keyDerivationFunctionParameters.uuid) {
+ return engine
+ }
+ }
+ Log.i(TAG, "Unable to retrieve KDF engine")
+ return null
+ }
+
+ override val kdfAvailableList: List = listOf(
+ KdfFactory.aesKdf,
+ KdfFactory.argon2dKdf,
+ KdfFactory.argon2idKdf
+ )
+
var kdfParameters: KdfParameters? = null
- private var kdfList: MutableList = ArrayList()
private var numKeyEncRounds: Long = 0
- var publicCustomData = VariantDictionary()
+
+ fun randomize() {
+ kdfParameters?.let {
+ kdfEngine?.randomize(it)
+ }
+ }
+
+ var compressionAlgorithm = CompressionAlgorithm.GZip
+
private val mFieldReferenceEngine = FieldReferencesEngine(this)
private val mTemplateEngine = TemplateEngineCompatible(this)
- var kdbxVersion = UnsignedInt(0)
var name = ""
var nameChanged = DateInstant()
- // TODO change setting date
- var settingsChanged = DateInstant()
var description = ""
var descriptionChanged = DateInstant()
var defaultUserName = ""
var defaultUserNameChanged = DateInstant()
-
- // TODO last change date
+ var settingsChanged = DateInstant()
var keyLastChanged = DateInstant()
var keyChangeRecDays: Long = -1
var keyChangeForceDays: Long = 1
@@ -115,17 +145,13 @@ class DatabaseKDBX : DatabaseVersioned {
var lastSelectedGroupUUID = UUID_ZERO
var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig()
- val deletedObjects = ArrayList()
+ val deletedObjects = HashSet()
+
+ var publicCustomData = VariantDictionary()
val customData = CustomData()
var localizedAppName = "KeePassDX"
- init {
- kdfList.add(KdfFactory.aesKdf)
- kdfList.add(KdfFactory.argon2dKdf)
- kdfList.add(KdfFactory.argon2idKdf)
- }
-
constructor()
/**
@@ -148,6 +174,8 @@ class DatabaseKDBX : DatabaseVersioned {
}
}
+ var kdbxVersion = UnsignedInt(0)
+
override val version: String
get() {
val kdbxStringVersion = when(kdbxVersion) {
@@ -159,38 +187,10 @@ class DatabaseKDBX : DatabaseVersioned {
return "V2 - KDBX$kdbxStringVersion"
}
- override val kdfEngine: KdfEngine?
- get() = try {
- getEngineKDBX4(kdfParameters)
- } catch (unknownKDF: UnknownKDF) {
- Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
- null
- }
-
- override val kdfAvailableList: List
- get() = kdfList
-
- @Throws(UnknownKDF::class)
- fun getEngineKDBX4(kdfParameters: KdfParameters?): KdfEngine {
- val unknownKDFException = UnknownKDF()
- if (kdfParameters == null) {
- throw unknownKDFException
- }
- for (engine in kdfList) {
- if (engine.uuid == kdfParameters.uuid) {
- return engine
- }
- }
- throw unknownKDFException
- }
-
- val availableCompressionAlgorithms: List
- get() {
- val list = ArrayList()
- list.add(CompressionAlgorithm.None)
- list.add(CompressionAlgorithm.GZip)
- return list
- }
+ val availableCompressionAlgorithms: List = listOf(
+ CompressionAlgorithm.None,
+ CompressionAlgorithm.GZip
+ )
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
@@ -245,15 +245,6 @@ class DatabaseKDBX : DatabaseVersioned {
}
}
- override val availableEncryptionAlgorithms: List
- get() {
- val list = ArrayList()
- list.add(EncryptionAlgorithm.AESRijndael)
- list.add(EncryptionAlgorithm.Twofish)
- list.add(EncryptionAlgorithm.ChaCha20)
- return list
- }
-
override var numberKeyEncryptionRounds: Long
get() {
val kdfEngine = kdfEngine
@@ -305,7 +296,7 @@ class DatabaseKDBX : DatabaseVersioned {
// Retrieve recycle bin in index
val recycleBin: GroupKDBX?
- get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID)
+ get() = getGroupByUUID(recycleBinUUID)
val lastSelectedGroup: GroupKDBX?
get() = getGroupByUUID(lastSelectedGroupUUID)
@@ -313,17 +304,14 @@ class DatabaseKDBX : DatabaseVersioned {
val lastTopVisibleGroup: GroupKDBX?
get() = getGroupByUUID(lastTopVisibleGroupUUID)
- fun setDataEngine(dataEngine: CipherEngine) {
- this.dataEngine = dataEngine
- }
-
override fun getStandardIcon(iconId: Int): IconImageStandard {
return this.iconsManager.getIcon(iconId)
}
fun buildNewCustomIcon(customIconId: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) {
- iconsManager.buildNewCustomIcon(customIconId, result)
+ // Create a binary file for a brand new custom icon
+ addCustomIcon(customIconId, "", null, false, result)
}
fun addCustomIcon(customIconId: UUID? = null,
@@ -331,14 +319,21 @@ class DatabaseKDBX : DatabaseVersioned {
lastModificationTime: DateInstant?,
smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) {
- iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
+ iconsManager.addCustomIcon(customIconId, name, lastModificationTime, { uniqueBinaryId ->
+ // Create a byte array for better performance with small data
+ binaryCache.getBinaryData(uniqueBinaryId, smallSize)
+ }, result)
+ }
+
+ fun removeCustomIcon(iconUuid: UUID) {
+ iconsManager.removeCustomIcon(iconUuid, binaryCache)
}
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
return iconsManager.isCustomIconBinaryDuplicate(binary)
}
- fun getCustomIcon(iconUuid: UUID): IconImageCustom {
+ fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
return this.iconsManager.getIcon(iconUuid)
}
@@ -355,7 +350,6 @@ class DatabaseKDBX : DatabaseVersioned {
val templatesGroup = firstGroupWithValidName
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id
- entryTemplatesGroupChanged = templatesGroup.lastModificationTime
} else {
removeTemplatesGroup()
}
@@ -363,7 +357,6 @@ class DatabaseKDBX : DatabaseVersioned {
fun removeTemplatesGroup() {
entryTemplatesGroup = UUID_ZERO
- entryTemplatesGroupChanged = DateInstant()
mTemplateEngine.clearCache()
}
@@ -475,28 +468,30 @@ class DatabaseKDBX : DatabaseVersioned {
@Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray) {
- kdfParameters?.let { keyDerivationFunctionParameters ->
- val kdfEngine = getEngineKDBX4(keyDerivationFunctionParameters)
+ kdfEngine?.let { keyDerivationFunctionEngine ->
+ kdfParameters?.let { keyDerivationFunctionParameters ->
- var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
- if (transformedMasterKey.size != 32) {
- transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
- }
+ var transformedMasterKey =
+ keyDerivationFunctionEngine.transform(masterKey, keyDerivationFunctionParameters)
+ if (transformedMasterKey.size != 32) {
+ transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
+ }
- val cmpKey = ByteArray(65)
- System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
- System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
- finalKey = resizeKey(cmpKey, dataEngine.keyLength())
+ val cmpKey = ByteArray(65)
+ System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
+ System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
+ finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
- val messageDigest: MessageDigest
- try {
- messageDigest = MessageDigest.getInstance("SHA-512")
- cmpKey[64] = 1
- hmacKey = messageDigest.digest(cmpKey)
- } catch (e: NoSuchAlgorithmException) {
- throw IOException("No SHA-512 implementation")
- } finally {
- Arrays.fill(cmpKey, 0.toByte())
+ val messageDigest: MessageDigest
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-512")
+ cmpKey[64] = 1
+ hmacKey = messageDigest.digest(cmpKey)
+ } catch (e: NoSuchAlgorithmException) {
+ throw IOException("No SHA-512 implementation")
+ } finally {
+ Arrays.fill(cmpKey, 0.toByte())
+ }
}
}
}
@@ -724,14 +719,13 @@ class DatabaseKDBX : DatabaseVersioned {
firstGroupWithValidName
}
recycleBinUUID = recycleBinGroup.id
- recycleBinChanged = recycleBinGroup.lastModificationTime
+ recycleBinChanged = DateInstant()
}
}
fun removeRecycleBin() {
if (recycleBin != null) {
recycleBinUUID = UUID_ZERO
- recycleBinChanged = DateInstant()
}
}
@@ -753,38 +747,18 @@ class DatabaseKDBX : DatabaseVersioned {
return false
}
- fun recycle(group: GroupKDBX, resources: Resources) {
- ensureRecycleBinExists(resources)
- removeGroupFrom(group, group.parent)
- addGroupTo(group, recycleBin)
- group.afterAssignNewParent()
- }
-
- fun recycle(entry: EntryKDBX, resources: Resources) {
- ensureRecycleBinExists(resources)
- removeEntryFrom(entry, entry.parent)
- addEntryTo(entry, recycleBin)
- entry.afterAssignNewParent()
- }
-
- fun undoRecycle(group: GroupKDBX, origParent: GroupKDBX) {
- removeGroupFrom(group, recycleBin)
- addGroupTo(group, origParent)
- }
-
- fun undoRecycle(entry: EntryKDBX, origParent: GroupKDBX) {
- removeEntryFrom(entry, recycleBin)
- addEntryTo(entry, origParent)
- }
-
- fun getDeletedObjects(): List {
- return deletedObjects
+ fun getDeletedObject(nodeId: NodeId): DeletedObject? {
+ return deletedObjects.find { it.uuid == nodeId.id }
}
fun addDeletedObject(deletedObject: DeletedObject) {
this.deletedObjects.add(deletedObject)
}
+ fun addDeletedObject(objectId: UUID) {
+ addDeletedObject(DeletedObject(objectId))
+ }
+
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
super.addEntryTo(newEntry, parent)
mFieldReferenceEngine.clear()
@@ -797,23 +771,17 @@ class DatabaseKDBX : DatabaseVersioned {
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent)
- deletedObjects.add(DeletedObject(entryToRemove.id))
mFieldReferenceEngine.clear()
}
- override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
- super.undoDeleteEntryFrom(entry, origParent)
- deletedObjects.remove(DeletedObject(entry.id))
- }
-
fun containsPublicCustomData(): Boolean {
return publicCustomData.size() > 0
}
- fun buildNewAttachment(smallSize: Boolean,
- compression: Boolean,
- protection: Boolean,
- binaryPoolId: Int? = null): BinaryData {
+ fun buildNewBinaryAttachment(smallSize: Boolean,
+ compression: Boolean,
+ protection: Boolean,
+ binaryPoolId: Int? = null): BinaryData {
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
}.binary
@@ -830,6 +798,7 @@ class DatabaseKDBX : DatabaseVersioned {
}
private fun removeUnlinkedAttachments(binaries: List, clear: Boolean) {
+ // TODO check in icon pool
// Build binaries to remove with all binaries known
val binariesToRemove = ArrayList()
if (binaries.isEmpty()) {
@@ -866,11 +835,10 @@ class DatabaseKDBX : DatabaseVersioned {
return super.validatePasswordEncoding(password, containsKeyFile)
}
- override fun clearCache() {
+ override fun clearIndexes() {
try {
- super.clearCache()
+ super.clearIndexes()
mFieldReferenceEngine.clear()
- attachmentPool.clear()
} catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}
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 61d3a3e40..fb0b1313c 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
@@ -19,8 +19,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.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
@@ -44,47 +46,37 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned
> {
+
// Algorithm used to encrypt the database
- protected var algorithm: EncryptionAlgorithm? = null
+ abstract var encryptionAlgorithm: EncryptionAlgorithm
+ abstract val availableEncryptionAlgorithms: List
- abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine?
+ abstract val kdfEngine: KdfEngine?
+ abstract val kdfAvailableList: List
+ abstract var numberKeyEncryptionRounds: Long
- abstract val kdfAvailableList: List
+ protected abstract val passwordEncoding: String
var masterKey = ByteArray(32)
var finalKey: ByteArray? = null
protected set
+ abstract val version: String
+
/**
* To manage binaries in faster way
* Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var binaryCache = BinaryCache()
- val iconsManager = IconsManager(binaryCache)
- var attachmentPool = AttachmentPool(binaryCache)
+ var iconsManager = IconsManager()
+ var attachmentPool = AttachmentPool()
var changeDuplicateId = false
private var groupIndexes = LinkedHashMap, Group>()
private var entryIndexes = LinkedHashMap, Entry>()
- abstract val version: String
-
- protected abstract val passwordEncoding: String
-
- abstract var numberKeyEncryptionRounds: Long
-
- var encryptionAlgorithm: EncryptionAlgorithm
- get() {
- return algorithm ?: EncryptionAlgorithm.AESRijndael
- }
- set(algorithm) {
- this.algorithm = algorithm
- }
-
- abstract val availableEncryptionAlgorithms: List
-
var rootGroup: Group? = null
set(value) {
field = value
@@ -274,7 +266,7 @@ abstract class DatabaseVersioned<
this.entryIndexes.remove(entry.nodeId)
}
- open fun clearCache() {
+ open fun clearIndexes() {
this.groupIndexes.clear()
this.entryIndexes.clear()
}
@@ -304,7 +296,7 @@ abstract class DatabaseVersioned<
}
}
- fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
+ open fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
// Remove tree from parent tree
parent?.removeChildGroup(groupToRemove)
removeGroupIndex(groupToRemove)
@@ -331,15 +323,6 @@ abstract class DatabaseVersioned<
removeEntryIndex(entryToRemove)
}
- // TODO Delete group
- fun undoDeleteGroupFrom(group: Group, origParent: Group?) {
- addGroupTo(group, origParent)
- }
-
- open fun undoDeleteEntryFrom(entry: Entry, origParent: Group?) {
- addEntryTo(entry, origParent)
- }
-
abstract fun isInRecycleBin(group: Group): Boolean
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
@@ -350,6 +333,39 @@ abstract class DatabaseVersioned<
return true
}
+ fun clearIconsCache() {
+ iconsManager.doForEachCustomIcon { _, binary ->
+ try {
+ binary.clear(binaryCache)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to clear icon binary cache", e)
+ }
+ }
+ iconsManager.clear()
+ }
+
+ fun clearAttachmentsCache() {
+ attachmentPool.doForEachBinary { _, binary ->
+ try {
+ binary.clear(binaryCache)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to clear attachment binary cache", e)
+ }
+ }
+ attachmentPool.clear()
+ }
+
+ fun clearBinaries() {
+ binaryCache.clear()
+ }
+
+ fun clearAll() {
+ clearIndexes()
+ clearIconsCache()
+ clearAttachmentsCache()
+ clearBinaries()
+ }
+
companion object {
private const val TAG = "DatabaseVersioned"
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt
index d2ff9312e..4a2d7f7ca 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDB.kt
@@ -139,8 +139,9 @@ class EntryKDB : EntryVersioned, NodeKDBInterface
dest.writeInt(binaryDataId ?: -1)
}
- fun updateWith(source: EntryKDB) {
- super.updateWith(source)
+ fun updateWith(source: EntryKDB,
+ updateParents: Boolean = true) {
+ super.updateWith(source, updateParents)
title = source.title
username = source.username
password = source.password
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt
index 43d48fbed..638db71c2 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/entry/EntryKDBX.kt
@@ -110,8 +110,10 @@ class EntryKDBX : EntryVersioned, NodeKDBXInte
* Update with deep copy of each entry element
* @param source
*/
- fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
- super.updateWith(source)
+ fun updateWith(source: EntryKDBX,
+ copyHistory: Boolean = true,
+ updateParents: Boolean = true) {
+ super.updateWith(source, updateParents)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
customData = CustomData(source.customData)
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt
index 3d25c255f..e74fcd016 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDB.kt
@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned, NodeKDBInterface
dest.writeInt(groupFlags)
}
- fun updateWith(source: GroupKDB) {
- super.updateWith(source)
+ fun updateWith(source: GroupKDB,
+ updateParents: Boolean = true) {
+ super.updateWith(source, updateParents)
groupFlags = source.groupFlags
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt
index d2bc674f2..172091432 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupKDBX.kt
@@ -102,8 +102,9 @@ class GroupKDBX : GroupVersioned, NodeKDBXInte
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
}
- fun updateWith(source: GroupKDBX) {
- super.updateWith(source)
+ fun updateWith(source: GroupKDBX,
+ updateParents: Boolean = true) {
+ super.updateWith(source, updateParents)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersioned.kt
index c137d1979..e1ed865da 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersioned.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/group/GroupVersioned.kt
@@ -51,12 +51,15 @@ abstract class GroupVersioned
dest.writeString(titleGroup)
}
- protected fun updateWith(source: GroupVersioned) {
- super.updateWith(source)
+ protected fun updateWith(source: GroupVersioned,
+ updateParents: Boolean = true) {
+ super.updateWith(source, updateParents)
titleGroup = source.titleGroup
- removeChildren()
- childGroups.addAll(source.childGroups)
- childEntries.addAll(source.childEntries)
+ if (updateParents) {
+ removeChildren()
+ childGroups.addAll(source.childGroups)
+ childEntries.addAll(source.childEntries)
+ }
}
override var title: String
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt
index 5cff786a7..6f6b88d4f 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt
@@ -28,12 +28,12 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
import java.util.*
-class IconsManager(binaryCache: BinaryCache) {
+class IconsManager {
private val standardCache = List(NB_ICONS) {
IconImageStandard(it)
}
- private val customCache = CustomIconPool(binaryCache)
+ private val customCache = CustomIconPool()
fun getIcon(iconId: Int): IconImageStandard {
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
* Custom
*/
- fun buildNewCustomIcon(key: UUID? = null,
- result: (IconImageCustom, BinaryData?) -> Unit) {
- // Create a binary file for a brand new custom icon
- addCustomIcon(key, "", null, false, result)
- }
-
fun addCustomIcon(key: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
- smallSize: Boolean,
+ builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) {
- customCache.put(key, name, lastModificationTime, smallSize, result)
+ customCache.put(key, name, lastModificationTime, builder, result)
}
- fun getIcon(iconUuid: UUID): IconImageCustom {
- return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
+ fun getIcon(iconUuid: UUID): IconImageCustom? {
+ return customCache.getCustomIcon(iconUuid)
}
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return customCache.isBinaryDuplicate(binaryData)
}
- fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) {
+ fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
val binary = customCache[iconUuid]
customCache.remove(iconUuid)
try {
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
/**
* Clear the cache of icons
*/
- fun clearCache() {
- try {
- customCache.clear()
- } catch(e: Exception) {
- Log.e(TAG, "Unable to clear cache", e)
- }
+ fun clear() {
+ customCache.clear()
}
companion object {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt
index d2b5b4e6b..5bbd81cf9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/element/node/NodeVersioned.kt
@@ -68,9 +68,12 @@ abstract class NodeVersioned) {
+ protected fun updateWith(source: NodeVersioned,
+ updateParents: Boolean = true) {
this.nodeId = copyNodeId(source.nodeId)
- this.parent = source.parent
+ if (updateParents) {
+ this.parent = source.parent
+ }
this.icon = source.icon
this.creationTime = DateInstant(source.creationTime)
this.lastModificationTime = DateInstant(source.lastModificationTime)
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 736071a6e..07f69ab4e 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
@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
+ constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable)
}
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.file_not_found_content
constructor() : super()
+ constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
}
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
+ constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt
deleted file mode 100644
index 5daf59459..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/database/exception/UnknownKDF.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright 2019 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.exception
-
-import java.io.IOException
-
-class UnknownKDF : IOException("Unknown key derivation function")
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt
index 1bbeaff93..9d5122833 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/DatabaseHeaderKDBX.kt
@@ -256,8 +256,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
if (pbId == null || pbId.size != 16) {
throw IOException("Invalid cipher ID.")
}
-
- databaseV4.cipherUuid = bytes16ToUuid(pbId)
+ databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
}
private fun setTransformRound(roundsByte: ByteArray) {
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 cd013b4d7..7ff7e1294 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
@@ -21,16 +21,12 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Log
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
-import java.io.File
import java.io.InputStream
-abstract class DatabaseInput>
- (protected val cacheDirectory: File,
- protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
+abstract class DatabaseInput> (protected var mDatabase: D) {
private var startTimeKey = System.currentTimeMillis()
private var startTimeContent = System.currentTimeMillis()
@@ -49,17 +45,13 @@ abstract class DatabaseInput>
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
- loadedCipherKey: LoadedKey,
- progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean = false): D
+ progressTaskUpdater: ProgressTaskUpdater?): D
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
- loadedCipherKey: LoadedKey,
- progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean = false): D
+ progressTaskUpdater: ProgressTaskUpdater?): 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 de953580a..7a019bbca 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
@@ -24,7 +24,6 @@ import android.graphics.Color
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.DateInstant
-import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -46,21 +45,15 @@ import kotlin.collections.HashMap
/**
* Load a KDB database file.
*/
-class DatabaseInputKDB(cacheDirectory: File,
- isRAMSufficient: (memoryWanted: Long) -> Boolean)
- : DatabaseInput(cacheDirectory, isRAMSufficient) {
-
- private lateinit var mDatabase: DatabaseKDB
+class DatabaseInputKDB(database: DatabaseKDB)
+ : DatabaseInput(database) {
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
- loadedCipherKey: LoadedKey,
- progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean): DatabaseKDB {
- return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
- mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
+ progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
+ return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@@ -68,11 +61,8 @@ class DatabaseInputKDB(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
- loadedCipherKey: LoadedKey,
- progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean): DatabaseKDB {
- return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
- mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
+ progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
+ return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.masterKey = masterKey
}
}
@@ -80,7 +70,6 @@ class DatabaseInputKDB(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try {
@@ -107,10 +96,6 @@ class DatabaseInputKDB(cacheDirectory: File,
throw VersionDatabaseException()
}
- mDatabase = DatabaseKDB()
- mDatabase.binaryCache.cacheDirectory = cacheDirectory
-
- mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke()
// Select algorithm
@@ -281,7 +266,7 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
- val binaryData = mDatabase.buildNewAttachment()
+ val binaryData = mDatabase.buildNewBinaryAttachment()
entry.putBinary(binaryData, mDatabase.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
cipherInputStream.readBytes(fieldSize) { buffer ->
@@ -346,16 +331,16 @@ class DatabaseInputKDB(cacheDirectory: File,
stopContentTimer()
} catch (e: LoadDatabaseException) {
- mDatabase.clearCache()
+ mDatabase.clearAll()
throw e
} catch (e: IOException) {
- mDatabase.clearCache()
+ mDatabase.clearAll()
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
- mDatabase.clearCache()
+ mDatabase.clearAll()
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
- mDatabase.clearCache()
+ mDatabase.clearAll()
throw LoadDatabaseException(e)
}
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 2103fb149..aaabe0be3 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
@@ -22,19 +22,17 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.StreamCipher
-import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
-import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.HmacBlock
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.binary.BinaryData
-import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
+import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -50,7 +48,6 @@ import com.kunzisoft.keepass.utils.*
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
-import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
@@ -63,12 +60,10 @@ import javax.crypto.CipherInputStream
import javax.crypto.Mac
import kotlin.math.min
-class DatabaseInputKDBX(cacheDirectory: File,
- isRAMSufficient: (memoryWanted: Long) -> Boolean)
- : DatabaseInput(cacheDirectory, isRAMSufficient) {
+class DatabaseInputKDBX(database: DatabaseKDBX)
+ : DatabaseInput(database) {
private var randomStream: StreamCipher? = null
- private lateinit var mDatabase: DatabaseKDBX
private var hashOfHeader: ByteArray? = null
@@ -97,15 +92,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var entryCustomDataKey: String? = null
private var entryCustomDataValue: String? = null
+ private var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
+
+ fun setMethodToCheckIfRAMIsSufficient(method: (memoryWanted: Long) -> Boolean) {
+ this.isRAMSufficient = method
+ }
+
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
- loadedCipherKey: LoadedKey,
- progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean): DatabaseKDBX {
- return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
- mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
+ progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
+ return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@@ -113,11 +111,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
- loadedCipherKey: LoadedKey,
- progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean): DatabaseKDBX {
- return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
- mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
+ progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
+ return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.masterKey = masterKey
}
}
@@ -125,14 +120,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
- fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try {
startKeyTimer(progressTaskUpdater)
- mDatabase = DatabaseKDBX()
- mDatabase.binaryCache.cacheDirectory = cacheDirectory
-
- mDatabase.changeDuplicateId = fixDuplicateUUID
val header = DatabaseHeaderKDBX(mDatabase)
@@ -148,13 +138,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
stopKeyTimer()
startContentTimer(progressTaskUpdater)
- val engine: CipherEngine
val cipher: Cipher
try {
- engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
+ val engine = mDatabase.encryptionAlgorithm.cipherEngine
engine.forcePaddingCompatibility = true
- mDatabase.setDataEngine(engine)
- mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
engine.forcePaddingCompatibility = false
} catch (e: Exception) {
@@ -288,7 +275,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
val byteLength = size - 1
// No compression at this level
- val protectedBinary = mDatabase.buildNewAttachment(
+ val protectedBinary = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
dataInputStream.readBytes(byteLength) { buffer ->
@@ -524,7 +511,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
- ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
+ val iconUUID = readUuid(xpp)
+ ctxGroup?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
ctxGroup?.tags = readTags(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
@@ -583,7 +571,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
- ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
+ val iconUUID = readUuid(xpp)
+ ctxEntry?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
ctxEntry?.foregroundColor = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
@@ -704,7 +693,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
- ctxDeletedObject?.setDeletionTime(readDateInstant(xpp))
+ ctxDeletedObject?.deletionTime = readDateInstant(xpp)
} else {
readUnknown(xpp)
}
@@ -1009,7 +998,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
var binaryRetrieve = mDatabase.attachmentPool[id]
// Create empty binary if not retrieved in pool
if (binaryRetrieve == null) {
- binaryRetrieve = mDatabase.buildNewAttachment(
+ binaryRetrieve = mDatabase.buildNewBinaryAttachment(
smallSize = false,
compression = false,
protection = false,
@@ -1049,7 +1038,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
return null
// Build the new binary and compress
- val binaryAttachment = mDatabase.buildNewAttachment(
+ val binaryAttachment = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
try {
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt
index 3c98b84cc..d9c8f4dae 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt
@@ -25,7 +25,6 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
-import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.stream.MacOutputStream
@@ -72,7 +71,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
mos.write4BytesUInt(header.version)
- writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.cipherUuid))
+ writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.encryptionAlgorithm.uuid))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
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 9b92122ca..96cfe0c1c 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
@@ -263,7 +263,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
private fun setDefaultUsername(entryKDB: EntryKDB) {
- val binaryData = mDatabaseKDB.buildNewAttachment()
+ val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
@@ -271,7 +271,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
private fun setDatabaseColor(entryKDB: EntryKDB) {
- val binaryData = mDatabaseKDB.buildNewAttachment()
+ val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
var reversColor = Color.BLACK
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 885b882a6..ada1eff66 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
@@ -26,7 +26,6 @@ import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
-import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
@@ -39,7 +38,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
-import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
@@ -76,7 +74,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
try {
try {
- engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
+ engine = mDatabaseKDBX.encryptionAlgorithm.cipherEngine
} catch (e: NoSuchAlgorithmException) {
throw DatabaseOutputException("No such cipher", e)
}
@@ -240,6 +238,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
}
+ writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
@@ -300,13 +299,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (mDatabaseKDBX.kdfParameters == null) {
mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters
- }
-
- try {
- val kdf = mDatabaseKDBX.getEngineKDBX4(mDatabaseKDBX.kdfParameters)
- kdf.randomize(mDatabaseKDBX.kdfParameters!!)
- } catch (unknownKDF: UnknownKDF) {
- Log.e(TAG, "Unable to retrieve header", unknownKDF)
+ mDatabaseKDBX.randomize()
}
if (header.version.isBefore(FILE_VERSION_40)) {
@@ -591,7 +584,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
- writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
+ writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
}
@@ -617,7 +610,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
- private fun writeDeletedObjects(value: List) {
+ private fun writeDeletedObjects(value: Collection) {
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
for (pdo in value) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt b/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt
new file mode 100644
index 000000000..3634373e2
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/database/merge/DatabaseKDBXMerger.kt
@@ -0,0 +1,473 @@
+/*
+ * 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.merge
+
+import com.kunzisoft.keepass.database.action.node.NodeHandler
+import com.kunzisoft.keepass.database.element.Attachment
+import com.kunzisoft.keepass.database.element.CustomData
+import com.kunzisoft.keepass.database.element.DateInstant
+import com.kunzisoft.keepass.database.element.database.DatabaseKDB
+import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
+import com.kunzisoft.keepass.database.element.entry.EntryKDB
+import com.kunzisoft.keepass.database.element.entry.EntryKDBX
+import com.kunzisoft.keepass.database.element.group.GroupKDB
+import com.kunzisoft.keepass.database.element.group.GroupKDBX
+import com.kunzisoft.keepass.database.element.node.NodeId
+import com.kunzisoft.keepass.database.element.node.NodeIdUUID
+import com.kunzisoft.keepass.utils.readAllBytes
+import java.io.IOException
+import java.util.*
+
+class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
+
+ var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
+
+ /**
+ * Merge a KDB database in a KDBX database, by default all data are copied from the KDB
+ */
+ fun merge(databaseToMerge: DatabaseKDB) {
+ // TODO Test KDB merge
+ val rootGroup = database.rootGroup
+ val rootGroupId = rootGroup?.nodeId
+ val rootGroupToMerge = databaseToMerge.rootGroup
+ val rootGroupIdToMerge = rootGroupToMerge?.nodeId
+
+ if (rootGroupId == null || rootGroupIdToMerge == null) {
+ throw IOException("Database is not open")
+ }
+
+ // Merge children
+ rootGroupToMerge.doForEachChild(
+ object : NodeHandler() {
+ override fun operate(node: EntryKDB): Boolean {
+ mergeEntry(rootGroup.nodeId, node, databaseToMerge)
+ return true
+ }
+ },
+ object : NodeHandler() {
+ override fun operate(node: GroupKDB): Boolean {
+ mergeGroup(rootGroup.nodeId, node, databaseToMerge)
+ return true
+ }
+ }
+ )
+ }
+
+ /**
+ * Utility method to transform KDB id nodes in KDBX id nodes
+ */
+ private fun getNodeIdUUIDFrom(seed: NodeId, intId: NodeId): NodeId {
+ val seedUUID = seed.id
+ val idInt = intId.id
+ return NodeIdUUID(UUID(seedUUID.mostSignificantBits, seedUUID.leastSignificantBits + idInt))
+ }
+
+ /**
+ * Utility method to merge a KDB entry
+ */
+ private fun mergeEntry(seed: NodeId, nodeToMerge: EntryKDB, databaseToMerge: DatabaseKDB) {
+ val entryId: NodeId = nodeToMerge.nodeId
+ val entry = database.getEntryById(entryId)
+
+ databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
+ // Retrieve parent in current database
+ var parentEntryToMerge: GroupKDBX? = null
+ srcEntryToMerge.parent?.nodeId?.let {
+ val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
+ parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
+ }
+ val entryToMerge = EntryKDBX().apply {
+ this.nodeId = srcEntryToMerge.nodeId
+ this.icon = srcEntryToMerge.icon
+ this.creationTime = DateInstant(srcEntryToMerge.creationTime)
+ this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
+ this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
+ this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
+ this.expires = srcEntryToMerge.expires
+ this.title = srcEntryToMerge.title
+ this.username = srcEntryToMerge.username
+ this.password = srcEntryToMerge.password
+ this.url = srcEntryToMerge.url
+ this.notes = srcEntryToMerge.notes
+ // TODO attachment
+ }
+ if (entry != null) {
+ entry.updateWith(entryToMerge, false)
+ } else if (parentEntryToMerge != null) {
+ database.addEntryTo(entryToMerge, parentEntryToMerge)
+ }
+ }
+ }
+
+ /**
+ * Utility method to merge a KDB group
+ */
+ private fun mergeGroup(seed: NodeId, nodeToMerge: GroupKDB, databaseToMerge: DatabaseKDB) {
+ val groupId: NodeId = nodeToMerge.nodeId
+ val group = database.getGroupById(getNodeIdUUIDFrom(seed, groupId))
+
+ databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
+ // Retrieve parent in current database
+ var parentGroupToMerge: GroupKDBX? = null
+ srcGroupToMerge.parent?.nodeId?.let {
+ val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
+ parentGroupToMerge = database.getGroupById(parentGroupIdToMerge)
+ }
+ val groupToMerge = GroupKDBX().apply {
+ this.nodeId = getNodeIdUUIDFrom(seed, srcGroupToMerge.nodeId)
+ this.icon = srcGroupToMerge.icon
+ this.creationTime = DateInstant(srcGroupToMerge.creationTime)
+ this.lastModificationTime = DateInstant(srcGroupToMerge.lastModificationTime)
+ this.lastAccessTime = DateInstant(srcGroupToMerge.lastAccessTime)
+ this.expiryTime = DateInstant(srcGroupToMerge.expiryTime)
+ this.expires = srcGroupToMerge.expires
+ this.title = srcGroupToMerge.title
+ }
+ if (group != null) {
+ group.updateWith(groupToMerge, false)
+ } else if (parentGroupToMerge != null) {
+ database.addGroupTo(groupToMerge, parentGroupToMerge)
+ }
+ }
+ }
+
+ /**
+ * Merge a KDB> database in a KDBX database,
+ * Try to take into account the modification date of each element
+ * To make a merge as accurate as possible
+ */
+ fun merge(databaseToMerge: DatabaseKDBX) {
+
+ // Merge settings
+ if (database.nameChanged.date.before(databaseToMerge.nameChanged.date)) {
+ database.name = databaseToMerge.name
+ database.nameChanged = databaseToMerge.nameChanged
+ }
+ if (database.descriptionChanged.date.before(databaseToMerge.descriptionChanged.date)) {
+ database.description = databaseToMerge.description
+ database.descriptionChanged = databaseToMerge.descriptionChanged
+ }
+ if (database.defaultUserNameChanged.date.before(databaseToMerge.defaultUserNameChanged.date)) {
+ database.defaultUserName = databaseToMerge.defaultUserName
+ database.defaultUserNameChanged = databaseToMerge.defaultUserNameChanged
+ }
+ if (database.keyLastChanged.date.before(databaseToMerge.keyLastChanged.date)) {
+ database.keyChangeRecDays = databaseToMerge.keyChangeRecDays
+ database.keyChangeForceDays = databaseToMerge.keyChangeForceDays
+ database.isKeyChangeForceOnce = databaseToMerge.isKeyChangeForceOnce
+ database.keyLastChanged = databaseToMerge.keyLastChanged
+ }
+ if (database.recycleBinChanged.date.before(databaseToMerge.recycleBinChanged.date)) {
+ database.isRecycleBinEnabled = databaseToMerge.isRecycleBinEnabled
+ database.recycleBinUUID = databaseToMerge.recycleBinUUID
+ database.recycleBinChanged = databaseToMerge.recycleBinChanged
+ }
+ if (database.entryTemplatesGroupChanged.date.before(databaseToMerge.entryTemplatesGroupChanged.date)) {
+ database.entryTemplatesGroup = databaseToMerge.entryTemplatesGroup
+ database.entryTemplatesGroupChanged = databaseToMerge.entryTemplatesGroupChanged
+ }
+ if (database.settingsChanged.date.before(databaseToMerge.settingsChanged.date)) {
+ database.color = databaseToMerge.color
+ database.compressionAlgorithm = databaseToMerge.compressionAlgorithm
+ database.historyMaxItems = databaseToMerge.historyMaxItems
+ database.historyMaxSize = databaseToMerge.historyMaxSize
+ database.encryptionAlgorithm = databaseToMerge.encryptionAlgorithm
+ database.kdfParameters = databaseToMerge.kdfParameters
+ database.numberKeyEncryptionRounds = databaseToMerge.numberKeyEncryptionRounds
+ database.memoryUsage = databaseToMerge.memoryUsage
+ database.parallelism = databaseToMerge.parallelism
+ database.settingsChanged = databaseToMerge.settingsChanged
+ }
+
+ val rootGroup = database.rootGroup
+ val rootGroupId = rootGroup?.nodeId
+ val rootGroupToMerge = databaseToMerge.rootGroup
+ val rootGroupIdToMerge = rootGroupToMerge?.nodeId
+
+ if (rootGroupId == null || rootGroupIdToMerge == null) {
+ throw IOException("Database is not open")
+ }
+
+ // UUID of the root group to merge is unknown
+ if (database.getGroupById(rootGroupIdToMerge) == null) {
+ // Change it to copy children database root
+ databaseToMerge.removeGroupIndex(rootGroupToMerge)
+ rootGroupToMerge.nodeId = rootGroupId
+ databaseToMerge.addGroupIndex(rootGroupToMerge)
+ }
+
+ // Merge root group
+ if (rootGroup.lastModificationTime.date
+ .before(rootGroupToMerge.lastModificationTime.date)) {
+ rootGroup.updateWith(rootGroupToMerge, updateParents = false)
+ }
+ // Merge children
+ rootGroupToMerge.doForEachChild(
+ object : NodeHandler() {
+ override fun operate(node: EntryKDBX): Boolean {
+ mergeEntry(node, databaseToMerge)
+ return true
+ }
+ },
+ object : NodeHandler() {
+ override fun operate(node: GroupKDBX): Boolean {
+ mergeGroup(node, databaseToMerge)
+ return true
+ }
+ }
+ )
+
+ // Merge custom data in database header
+ mergeCustomData(database.customData, databaseToMerge.customData)
+
+ // Merge icons
+ databaseToMerge.iconsManager.doForEachCustomIcon { iconImageCustom, binaryData ->
+ val customIconUuid = iconImageCustom.uuid
+ // If custom icon not present, add it
+ val customIcon = database.iconsManager.getIcon(customIconUuid)
+ if (customIcon == null) {
+ database.addCustomIcon(
+ customIconUuid,
+ iconImageCustom.name,
+ iconImageCustom.lastModificationTime,
+ false
+ ) { _, newBinaryData ->
+ binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
+ newBinaryData?.getOutputDataStream(database.binaryCache).use { outputStream ->
+ inputStream.readAllBytes { buffer ->
+ outputStream?.write(buffer)
+ }
+ }
+ }
+ }
+ } else {
+ val customIconModification = customIcon.lastModificationTime
+ val customIconToMerge = databaseToMerge.iconsManager.getIcon(customIconUuid)
+ val customIconModificationToMerge = customIconToMerge?.lastModificationTime
+ if (customIconModification != null && customIconModificationToMerge != null) {
+ if (customIconModification.date.before(customIconModificationToMerge.date)) {
+ customIcon.updateWith(customIconToMerge)
+ }
+ } else if (customIconModificationToMerge != null) {
+ customIcon.updateWith(customIconToMerge)
+ }
+ }
+ }
+
+ // Manage deleted objects
+ databaseToMerge.deletedObjects.forEach { deletedObject ->
+ val deletedObjectId = deletedObject.uuid
+ val databaseEntry = database.getEntryById(deletedObjectId)
+ val databaseGroup = database.getGroupById(deletedObjectId)
+ val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
+ val databaseIconModificationTime = databaseIcon?.lastModificationTime
+ if (databaseEntry != null
+ && deletedObject.deletionTime.date
+ .after(databaseEntry.lastModificationTime.date)) {
+ database.removeEntryFrom(databaseEntry, databaseEntry.parent)
+ }
+ if (databaseGroup != null
+ && deletedObject.deletionTime.date
+ .after(databaseGroup.lastModificationTime.date)) {
+ database.removeGroupFrom(databaseGroup, databaseGroup.parent)
+ }
+ if (databaseIcon != null
+ && (
+ databaseIconModificationTime == null
+ || (deletedObject.deletionTime.date.after(databaseIconModificationTime.date))
+ )
+ ) {
+ database.removeCustomIcon(deletedObjectId)
+ }
+ // Attachments are removed and optimized during the database save
+ }
+ }
+
+ /**
+ * Merge [customDataToMerge] in [customData]
+ */
+ private fun mergeCustomData(customData: CustomData, customDataToMerge: CustomData) {
+ customDataToMerge.doForEachItems { customDataItemToMerge ->
+ val customDataItem = customData.get(customDataItemToMerge.key)
+ if (customDataItem == null) {
+ customData.put(customDataItemToMerge)
+ } else {
+ val customDataItemModification = customDataItem.lastModificationTime
+ val customDataItemToMergeModification = customDataItemToMerge.lastModificationTime
+ if (customDataItemModification != null && customDataItemToMergeModification != null) {
+ if (customDataItemModification.date
+ .before(customDataItemToMergeModification.date)) {
+ customData.put(customDataItemToMerge)
+ }
+ } else {
+ customData.put(customDataItemToMerge)
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility method to merge a KDBX entry
+ */
+ private fun mergeEntry(nodeToMerge: EntryKDBX, databaseToMerge: DatabaseKDBX) {
+ val entryId = nodeToMerge.nodeId
+ val entry = database.getEntryById(entryId)
+ val deletedObject = database.getDeletedObject(entryId)
+
+ databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
+ // Retrieve parent in current database
+ var parentEntryToMerge: GroupKDBX? = null
+ srcEntryToMerge.parent?.nodeId?.let {
+ parentEntryToMerge = database.getGroupById(it)
+ }
+ val entryToMerge = EntryKDBX().apply {
+ updateWith(srcEntryToMerge, copyHistory = true, updateParents = false)
+ }
+
+ // Copy attachments in main pool
+ val newAttachments = mutableListOf()
+ entryToMerge.getAttachments(databaseToMerge.attachmentPool).forEach { attachment ->
+ val binarySize = attachment.binaryData.getSize()
+ val binaryData = database.buildNewBinaryAttachment(
+ isRAMSufficient.invoke(binarySize),
+ attachment.binaryData.isCompressed,
+ attachment.binaryData.isProtected
+ )
+ attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
+ binaryData.getOutputDataStream(database.binaryCache).use { outputStream ->
+ inputStream.readAllBytes { buffer ->
+ outputStream.write(buffer)
+ }
+ }
+ }
+ newAttachments.add(Attachment(attachment.name, binaryData))
+ }
+ entryToMerge.removeAttachments()
+ newAttachments.forEach { newAttachment ->
+ entryToMerge.putAttachment(newAttachment, database.attachmentPool)
+ }
+
+ if (entry == null) {
+ // If it's a deleted object, but another instance was updated
+ // If entry parent to add exists and in current database
+ if ((deletedObject == null
+ || deletedObject.deletionTime.date
+ .before(entryToMerge.lastModificationTime.date))
+ && parentEntryToMerge != null) {
+ database.addEntryTo(entryToMerge, parentEntryToMerge)
+ }
+ } else {
+ // Merge independently custom data
+ mergeCustomData(entry.customData, entryToMerge.customData)
+ // Merge by modification time
+ if (entry.lastModificationTime.date
+ .before(entryToMerge.lastModificationTime.date)
+ ) {
+ addHistory(entry, entryToMerge)
+ if (parentEntryToMerge == entry.parent) {
+ entry.updateWith(entryToMerge, copyHistory = true, updateParents = false)
+ } else {
+ // Update entry with databaseEntryToMerge and merge history
+ database.removeEntryFrom(entry, entry.parent)
+ if (parentEntryToMerge != null) {
+ database.addEntryTo(entryToMerge, parentEntryToMerge)
+ }
+ }
+ } else if (entry.lastModificationTime.date
+ .after(entryToMerge.lastModificationTime.date)
+ ) {
+ addHistory(entryToMerge, entry)
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility method to merge an history from an [entryA] to an [entryB],
+ * [entryB] is modified
+ */
+ private fun addHistory(entryA: EntryKDBX, entryB: EntryKDBX) {
+ // Keep entry as history if already not present
+ entryA.history.forEach { history ->
+ // If history not present
+ if (!entryB.history.any {
+ it.lastModificationTime == history.lastModificationTime
+ }) {
+ entryB.addEntryToHistory(history)
+ }
+ }
+ // Last entry not present
+ if (entryB.history.find {
+ it.lastModificationTime == entryA.lastModificationTime
+ } == null) {
+ val history = EntryKDBX().apply {
+ updateWith(entryA, copyHistory = false, updateParents = false)
+ parent = null
+ }
+ entryB.addEntryToHistory(history)
+ }
+ }
+
+ /**
+ * Utility method to merge a KDBX group
+ */
+ private fun mergeGroup(nodeToMerge: GroupKDBX, databaseToMerge: DatabaseKDBX) {
+ val groupId = nodeToMerge.nodeId
+ val group = database.getGroupById(groupId)
+ val deletedObject = database.getDeletedObject(groupId)
+
+ databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
+ // Retrieve parent in current database
+ var parentGroupToMerge: GroupKDBX? = null
+ srcGroupToMerge.parent?.nodeId?.let {
+ parentGroupToMerge = database.getGroupById(it)
+ }
+ val groupToMerge = GroupKDBX().apply {
+ updateWith(srcGroupToMerge, updateParents = false)
+ }
+
+ if (group == null) {
+ // If group parent to add exists and in current database
+ if ((deletedObject == null
+ || deletedObject.deletionTime.date
+ .before(groupToMerge.lastModificationTime.date))
+ && parentGroupToMerge != null) {
+ database.addGroupTo(groupToMerge, parentGroupToMerge)
+ }
+ } else {
+ // Merge independently custom data
+ mergeCustomData(group.customData, groupToMerge.customData)
+ // Merge by modification time
+ if (group.lastModificationTime.date
+ .before(groupToMerge.lastModificationTime.date)
+ ) {
+ if (parentGroupToMerge == group.parent) {
+ group.updateWith(groupToMerge, false)
+ } else {
+ database.removeGroupFrom(group, group.parent)
+ if (parentGroupToMerge != null) {
+ database.addGroupTo(groupToMerge, parentGroupToMerge)
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ 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 fbb0ee680..81e41cc27 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt
@@ -226,6 +226,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database)
+ ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(database)
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database)
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
@@ -287,8 +288,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
} finally {
// Save the database info before performing action
- if (intentAction == ACTION_DATABASE_LOAD_TASK) {
- saveDatabaseInfo()
+ when (intentAction) {
+ ACTION_DATABASE_LOAD_TASK,
+ ACTION_DATABASE_MERGE_TASK,
+ ACTION_DATABASE_RELOAD_TASK -> {
+ saveDatabaseInfo()
+ }
}
val save = !database.isReadOnly
&& (intentAction == ACTION_DATABASE_SAVE
@@ -331,6 +336,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return when (intentAction) {
ACTION_DATABASE_LOAD_TASK,
+ ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK,
null -> {
START_STICKY
@@ -367,6 +373,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK,
+ ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
@@ -378,6 +385,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK,
+ ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> null
else -> null
}
@@ -385,6 +393,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mWarningId =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK
+ || intentAction == ACTION_DATABASE_MERGE_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
null
else
@@ -597,6 +606,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
+ private fun buildDatabaseMergeActionTask(database: Database): ActionRunnable {
+ return MergeDatabaseRunnable(
+ this,
+ database,
+ this
+ ) { result ->
+ // No need to add each info to reload database
+ result.data = Bundle()
+ }
+ }
+
private fun buildDatabaseReloadActionTask(database: Database): ActionRunnable {
return ReloadDatabaseRunnable(
this,
@@ -907,6 +927,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
+ const val ACTION_DATABASE_MERGE_TASK = "ACTION_DATABASE_MERGE_TASK"
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
index fca083eb4..eeed43b49 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt
@@ -51,6 +51,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: Database? = null
private var mDatabaseReadOnly: Boolean = false
+ private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true
private var mScreen: Screen? = null
@@ -115,6 +116,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.saveDatabase(save)
}
+ private fun mergeDatabase() {
+ mDatabaseViewModel.mergeDatabase(false)
+ }
+
private fun reloadDatabase() {
mDatabaseViewModel.reloadDatabase(false)
}
@@ -122,6 +127,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database
mDatabaseReadOnly = database?.isReadOnly == true
+ mMergeDataAllowed = database?.isMergeDataAllowed() == true
mDatabase?.let {
if (it.loaded) {
@@ -649,6 +655,9 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (mDatabaseReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false
}
+ if (!mMergeDataAllowed || mDatabaseReadOnly) {
+ menu.findItem(R.id.menu_merge_database)?.isVisible = false
+ }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -657,6 +666,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
saveDatabase(!mDatabaseReadOnly)
true
}
+ R.id.menu_merge_database -> {
+ mergeDatabase()
+ return true
+ }
R.id.menu_reload_database -> {
reloadDatabase()
return true
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt
index b41934591..cab7cb8bd 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.kt
@@ -63,13 +63,11 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
super.onDialogClosed(database, positiveResult)
if (positiveResult) {
database?.let {
- if (algorithmSelected != null) {
- val newAlgorithm = algorithmSelected
- val oldAlgorithm = database.encryptionAlgorithm
+ val newAlgorithm = algorithmSelected
+ val oldAlgorithm = database.encryptionAlgorithm
+ if (newAlgorithm != null) {
database.encryptionAlgorithm = newAlgorithm
-
- if (oldAlgorithm != null && newAlgorithm != null)
- saveEncryption(oldAlgorithm, newAlgorithm)
+ saveEncryption(oldAlgorithm, newAlgorithm)
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt
index 9e3dafcaa..8608b144a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseViewModel.kt
@@ -21,6 +21,9 @@ class DatabaseViewModel: ViewModel() {
val saveDatabase : LiveData get() = _saveDatabase
private val _saveDatabase = SingleLiveEvent()
+ val mergeDatabase : LiveData get() = _mergeDatabase
+ private val _mergeDatabase = SingleLiveEvent()
+
val reloadDatabase : LiveData get() = _reloadDatabase
private val _reloadDatabase = SingleLiveEvent()
@@ -84,6 +87,10 @@ class DatabaseViewModel: ViewModel() {
_saveDatabase.value = save
}
+ fun mergeDatabase(fixDuplicateUuid: Boolean) {
+ _mergeDatabase.value = fixDuplicateUuid
+ }
+
fun reloadDatabase(fixDuplicateUuid: Boolean) {
_reloadDatabase.value = fixDuplicateUuid
}
diff --git a/app/src/main/res/drawable/ic_merge_white_24dp.xml b/app/src/main/res/drawable/ic_merge_white_24dp.xml
new file mode 100644
index 000000000..e180205de
--- /dev/null
+++ b/app/src/main/res/drawable/ic_merge_white_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/database.xml b/app/src/main/res/menu/database.xml
index 43d5a5231..ce31d9e40 100644
--- a/app/src/main/res/menu/database.xml
+++ b/app/src/main/res/menu/database.xml
@@ -25,10 +25,16 @@
android:orderInCategory="95"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" />
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 861e6fb7c..6e5862a8a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -226,8 +226,9 @@
Cancel
Hide password
Lock database
- Save database
- Reload database
+ Save data
+ Merge data
+ Reload data
Open
Search
Show password
@@ -326,7 +327,8 @@
It is not recommended to add an empty keyfile.
The content of the keyfile should never be changed, and in the best case, should contain randomly generated data.
The information contained in your database file has been modified outside the app.
- Overwrite the external modifications by saving the database or reload it with the latest changes.
+ Merge the data, overwrite the external modifications by saving the database or reload the database with the latest changes.
+ Reloading the database will delete the locally modified data.
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.
Permission
diff --git a/art/merge_database.svg b/art/merge_database.svg
new file mode 100644
index 000000000..7e772e394
--- /dev/null
+++ b/art/merge_database.svg
@@ -0,0 +1,59 @@
+
+
diff --git a/fastlane/metadata/android/en-US/changelogs/93.txt b/fastlane/metadata/android/en-US/changelogs/93.txt
new file mode 100644
index 000000000..0211e20ad
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/93.txt
@@ -0,0 +1 @@
+ * Manage data merge #840 #977
\ No newline at end of file
diff --git a/fastlane/metadata/android/fr-FR/changelogs/93.txt b/fastlane/metadata/android/fr-FR/changelogs/93.txt
new file mode 100644
index 000000000..f20d389b4
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/93.txt
@@ -0,0 +1 @@
+ * Gestion de la fusion des données #840 #977
\ No newline at end of file