Merge branch 'feature/Merge_Data' into develop #840

This commit is contained in:
J-Jamet
2022-01-19 20:40:47 +01:00
48 changed files with 1217 additions and 527 deletions

View File

@@ -1,3 +1,6 @@
KeePassDX(3.2.0)
* Manage data merge #840 #977
KeePassDX(3.1.0) KeePassDX(3.1.0)
* Add breadcrumb * Add breadcrumb
* Add path in search results #1148 * Add path in search results #1148

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 31 targetSdkVersion 31
versionCode = 92 versionCode = 93
versionName = "3.1.0" versionName = "3.2.0"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -376,6 +376,9 @@ class EntryActivity : DatabaseLockActivity() {
menu?.findItem(R.id.menu_save_database)?.isVisible = false menu?.findItem(R.id.menu_save_database)?.isVisible = false
menu?.findItem(R.id.menu_edit)?.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) { if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_reload_database)?.isVisible = false menu?.findItem(R.id.menu_reload_database)?.isVisible = false
} }
@@ -455,6 +458,9 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_save_database -> { R.id.menu_save_database -> {
saveDatabase() saveDatabase()
} }
R.id.menu_merge_database -> {
mergeDatabase()
}
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
reloadDatabase() reloadDatabase()
} }

View File

@@ -970,6 +970,9 @@ class GroupActivity : DatabaseLockActivity(),
if (mDatabaseReadOnly) { if (mDatabaseReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false 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) { if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu) MenuUtil.defaultMenuInflater(inflater, menu)
} else { } else {
@@ -1093,6 +1096,10 @@ class GroupActivity : DatabaseLockActivity(),
saveDatabase() saveDatabase()
return true return true
} }
R.id.menu_merge_database -> {
mergeDatabase()
return true
}
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
reloadDatabase() reloadDatabase()
return true return true

View File

@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
private var mExitLock: Boolean = false private var mExitLock: Boolean = false
protected var mDatabaseReadOnly: Boolean = true protected var mDatabaseReadOnly: Boolean = true
protected var mMergeDataAllowed: Boolean = false
private var mAutoSaveEnable: Boolean = true private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null protected var mIconDrawableFactory: IconDrawableFactory? = null
@@ -87,9 +88,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save) mDatabaseTaskProvider?.startDatabaseSave(save)
} }
mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
} }
}
mDatabaseViewModel.saveName.observe(this) { mDatabaseViewModel.saveName.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save) mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
@@ -197,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
mDatabaseReadOnly = database.isReadOnly mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
mIconDrawableFactory = database.iconDrawableFactory mIconDrawableFactory = database.iconDrawableFactory
checkRegister() checkRegister()
@@ -212,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
) { ) {
super.onDatabaseActionFinished(database, actionTask, result) super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) { when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity // Reload the current activity
if (result.isSuccess) { if (result.isSuccess) {
@@ -254,9 +263,15 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(true) mDatabaseTaskProvider?.startDatabaseSave(true)
} }
fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(false)
}
fun reloadDatabase() { fun reloadDatabase() {
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(false) mDatabaseTaskProvider?.startDatabaseReload(false)
} }
}
fun createEntry(newEntry: Entry, fun createEntry(newEntry: Entry,
parent: Group) { parent: Group) {

View File

@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX 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.database.search.SearchHelper
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut import com.kunzisoft.keepass.view.strikeOut
@@ -112,7 +113,7 @@ class SearchEntryCursorAdapter(private val context: Context,
database.getStandardIcon(standardIconId) database.getStandardIcon(standardIconId)
}, },
{ customIconId -> { customIconId ->
database.getCustomIcon(customIconId) database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId)
} }
) )
} }
@@ -122,7 +123,7 @@ class SearchEntryCursorAdapter(private val context: Context,
database.getStandardIcon(standardIconId) database.getStandardIcon(standardIconId)
}, },
{ customIconId -> { customIconId ->
database.getCustomIcon(customIconId) database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId)
} }
) )
} }

View File

@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri) val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream) database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) { } catch (e: Exception) {
erase(mBackupKey) erase(mBackupKey)
setError(e) setError(e)

View File

@@ -27,6 +27,7 @@ import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R 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_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK 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_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_MOVE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_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 import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
@@ -354,6 +356,13 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_LOAD_TASK) , 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) { fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -361,6 +370,19 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_RELOAD_TASK) , 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, fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) { mainCredential: MainCredential) {

View File

@@ -60,7 +60,6 @@ class LoadDatabaseRunnable(private val context: Context,
{ memoryWanted -> { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
}, },
LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID, mFixDuplicateUUID,
progressTaskUpdater) progressTaskUpdater)
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?) private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() { : ActionRunnable() {
private var tempCipherKey: LoadedKey? = null
override fun onStartRun() { override fun onStartRun() {
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
// Clear before we load // Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context)) mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
mDatabase.wasReloaded = true mDatabase.wasReloaded = true
} }
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.reloadData(context.contentResolver, mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context),
{ memoryWanted -> { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
}, },
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
progressTaskUpdater) progressTaskUpdater)
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
setError(e) setError(e)
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} else { } else {
tempCipherKey = null
mDatabase.clearAndClose(context) mDatabase.clearAndClose(context)
} }
} }

View File

@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
foreachNode@ for(nodeToDelete in mNodesToDelete) { foreachNode@ for(nodeToDelete in mNodesToDelete) {
mOldParent = nodeToDelete.parent mOldParent = nodeToDelete.parent
mOldParent?.touch(modified = false, touchParents = true) nodeToDelete.touch(modified = true, touchParents = true)
when (nodeToDelete.type) { when (nodeToDelete.type) {
Type.GROUP -> { Type.GROUP -> {
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent // Remove Node from parent
mCanRecycle = database.canRecycle(groupToDelete) mCanRecycle = database.canRecycle(groupToDelete)
if (mCanRecycle) { if (mCanRecycle) {
groupToDelete.touch(modified = false, touchParents = true)
database.recycle(groupToDelete, context.resources) database.recycle(groupToDelete, context.resources)
groupToDelete.setPreviousParentGroup(mOldParent) groupToDelete.setPreviousParentGroup(mOldParent)
groupToDelete.touch(modified = true, touchParents = true)
} else { } else {
database.deleteGroup(groupToDelete) database.deleteGroup(groupToDelete)
} }
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent // Remove Node from parent
mCanRecycle = database.canRecycle(entryToDelete) mCanRecycle = database.canRecycle(entryToDelete)
if (mCanRecycle) { if (mCanRecycle) {
entryToDelete.touch(modified = false, touchParents = true)
database.recycle(entryToDelete, context.resources) database.recycle(entryToDelete, context.resources)
entryToDelete.setPreviousParentGroup(mOldParent) entryToDelete.setPreviousParentGroup(mOldParent)
entryToDelete.touch(modified = true, touchParents = true)
} else { } else {
database.deleteEntry(entryToDelete) database.deleteEntry(entryToDelete)
} }

View File

@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
foreachNode@ for(nodeToMove in mNodesToMove) { foreachNode@ for(nodeToMove in mNodesToMove) {
// Move node in new parent // Move node in new parent
mOldParent = nodeToMove.parent mOldParent = nodeToMove.parent
nodeToMove.touch(modified = true, touchParents = true)
when (nodeToMove.type) { when (nodeToMove.type) {
Type.GROUP -> { Type.GROUP -> {
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
// and if not in the current group // and if not in the current group
&& groupToMove != mNewParent && groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) { && !mNewParent.isContainedIn(groupToMove)) {
groupToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent) database.moveGroupTo(groupToMove, mNewParent)
groupToMove.setPreviousParentGroup(mOldParent) groupToMove.setPreviousParentGroup(mOldParent)
groupToMove.touch(modified = true, touchParents = true)
} else { } else {
// Only finish thread // Only finish thread
setError(MoveGroupDatabaseException()) setError(MoveGroupDatabaseException())
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
if (mOldParent != mNewParent if (mOldParent != mNewParent
// and root can contains entry // and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) { && (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
entryToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent) database.moveEntryTo(entryToMove, mNewParent)
entryToMove.setPreviousParentGroup(mOldParent) entryToMove.setPreviousParentGroup(mOldParent)
entryToMove.touch(modified = true, touchParents = true)
} else { } else {
// Only finish thread // Only finish thread
setError(MoveEntryDatabaseException()) setError(MoveEntryDatabaseException())

View File

@@ -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.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX 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.input.DatabaseInputKDBX
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX 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.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
@@ -94,6 +94,8 @@ class Database {
*/ */
var wasReloaded = false var wasReloaded = false
var dataModifiedSinceLastLoading = false
var loadTimestamp: Long? = null var loadTimestamp: Long? = null
private set private set
@@ -112,7 +114,7 @@ class Database {
private val iconsManager: IconsManager private val iconsManager: IconsManager
get() { get() {
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache) return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
} }
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) { fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
@@ -130,7 +132,7 @@ class Database {
return iconsManager.doForEachCustomIcon(action) return iconsManager.doForEachCustomIcon(action)
} }
fun getCustomIcon(iconId: UUID): IconImageCustom { fun getCustomIcon(iconId: UUID): IconImageCustom? {
return iconsManager.getIcon(iconId) return iconsManager.getIcon(iconId)
} }
@@ -144,11 +146,12 @@ class Database {
fun removeCustomIcon(customIcon: IconImageCustom) { fun removeCustomIcon(customIcon: IconImageCustom) {
iconDrawableFactory.clearFromCache(customIcon) iconDrawableFactory.clearFromCache(customIcon)
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) iconsManager.removeCustomIcon(customIcon.uuid, binaryCache)
mDatabaseKDBX?.addDeletedObject(customIcon.uuid)
} }
fun updateCustomIcon(customIcon: IconImageCustom) { fun updateCustomIcon(customIcon: IconImageCustom) {
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon) iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon)
} }
fun getTemplates(templateCreation: Boolean): List<Template> { fun getTemplates(templateCreation: Boolean): List<Template> {
@@ -212,6 +215,7 @@ class Database {
set(name) { set(name) {
mDatabaseKDBX?.name = name mDatabaseKDBX?.name = name
mDatabaseKDBX?.nameChanged = DateInstant() mDatabaseKDBX?.nameChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
val allowDescription: Boolean val allowDescription: Boolean
@@ -224,6 +228,7 @@ class Database {
set(description) { set(description) {
mDatabaseKDBX?.description = description mDatabaseKDBX?.description = description
mDatabaseKDBX?.descriptionChanged = DateInstant() mDatabaseKDBX?.descriptionChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var defaultUsername: String var defaultUsername: String
@@ -234,6 +239,7 @@ class Database {
mDatabaseKDB?.defaultUserName = username mDatabaseKDB?.defaultUserName = username
mDatabaseKDBX?.defaultUserName = username mDatabaseKDBX?.defaultUserName = username
mDatabaseKDBX?.defaultUserNameChanged = DateInstant() mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var customColor: Int? var customColor: Int?
@@ -253,6 +259,8 @@ class Database {
} else { } else {
ChromaUtil.getFormattedColorString(value, false) ChromaUtil.getFormattedColorString(value, false)
} }
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
val allowOTP: Boolean val allowOTP: Boolean
@@ -276,6 +284,8 @@ class Database {
value?.let { value?.let {
mDatabaseKDBX?.compressionAlgorithm = it mDatabaseKDBX?.compressionAlgorithm = it
} }
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
fun compressionForNewEntry(): Boolean { fun compressionForNewEntry(): Boolean {
@@ -292,6 +302,7 @@ class Database {
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm, fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) { newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression) mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
dataModifiedSinceLastLoading = true
} }
val allowNoMasterKey: Boolean val allowNoMasterKey: Boolean
@@ -306,14 +317,12 @@ class Database {
val availableEncryptionAlgorithms: List<EncryptionAlgorithm> val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
get() = mDatabaseKDB?.availableEncryptionAlgorithms ?: mDatabaseKDBX?.availableEncryptionAlgorithms ?: ArrayList() get() = mDatabaseKDB?.availableEncryptionAlgorithms ?: mDatabaseKDBX?.availableEncryptionAlgorithms ?: ArrayList()
var encryptionAlgorithm: EncryptionAlgorithm? var encryptionAlgorithm: EncryptionAlgorithm
get() = mDatabaseKDB?.encryptionAlgorithm ?: mDatabaseKDBX?.encryptionAlgorithm get() = mDatabaseKDB?.encryptionAlgorithm
set(algorithm) { ?: mDatabaseKDBX?.encryptionAlgorithm
algorithm?.let { ?: EncryptionAlgorithm.AESRijndael
mDatabaseKDBX?.encryptionAlgorithm = algorithm set(value) {
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine) mDatabaseKDBX?.encryptionAlgorithm = value
mDatabaseKDBX?.cipherUuid = algorithm.uuid
}
} }
val availableKdfEngines: List<KdfEngine> val availableKdfEngines: List<KdfEngine>
@@ -343,6 +352,8 @@ class Database {
set(numberRounds) { set(numberRounds) {
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var memoryUsage: Long var memoryUsage: Long
@@ -351,12 +362,16 @@ class Database {
} }
set(memory) { set(memory) {
mDatabaseKDBX?.memoryUsage = memory mDatabaseKDBX?.memoryUsage = memory
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var parallelism: Long var parallelism: Long
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
set(parallelism) { set(parallelism) {
mDatabaseKDBX?.parallelism = parallelism mDatabaseKDBX?.parallelism = parallelism
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var masterKey: ByteArray var masterKey: ByteArray
@@ -364,6 +379,8 @@ class Database {
set(masterKey) { set(masterKey) {
mDatabaseKDB?.masterKey = masterKey mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var rootGroup: Group? var rootGroup: Group?
@@ -414,6 +431,8 @@ class Database {
} }
set(value) { set(value) {
mDatabaseKDBX?.historyMaxItems = value mDatabaseKDBX?.historyMaxItems = value
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
var historyMaxSize: Long var historyMaxSize: Long
@@ -422,6 +441,8 @@ class Database {
} }
set(value) { set(value) {
mDatabaseKDBX?.historyMaxSize = value mDatabaseKDBX?.historyMaxSize = value
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
/** /**
@@ -442,15 +463,17 @@ class Database {
} else { } else {
mDatabaseKDBX?.removeRecycleBin() mDatabaseKDBX?.removeRecycleBin()
} }
mDatabaseKDBX?.recycleBinChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
val recycleBin: Group? val recycleBin: Group?
get() { get() {
mDatabaseKDB?.backupGroup?.let { mDatabaseKDB?.backupGroup?.let {
return Group(it) return getGroupById(it.nodeId) ?: Group(it)
} }
mDatabaseKDBX?.recycleBin?.let { mDatabaseKDBX?.recycleBin?.let {
return Group(it) return getGroupById(it.nodeId) ?: Group(it)
} }
return null return null
} }
@@ -460,8 +483,10 @@ class Database {
if (group != null) { if (group != null) {
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
} else { } else {
mDatabaseKDBX?.removeTemplatesGroup() mDatabaseKDBX?.removeRecycleBin()
} }
mDatabaseKDBX?.recycleBinChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
/** /**
@@ -477,6 +502,8 @@ class Database {
fun enableTemplates(enable: Boolean, templatesGroupName: String) { fun enableTemplates(enable: Boolean, templatesGroupName: String) {
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName) mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
val templatesGroup: Group? val templatesGroup: Group?
@@ -492,8 +519,10 @@ class Database {
if (group != null) { if (group != null) {
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
} else { } else {
mDatabaseKDBX?.entryTemplatesGroup mDatabaseKDBX?.removeTemplatesGroup()
} }
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
dataModifiedSinceLastLoading = true
} }
val groupNamesNotAllowed: List<String> val groupNamesNotAllowed: List<String>
@@ -520,6 +549,7 @@ class Database {
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
this.loaded = true this.loaded = true
this.dataModifiedSinceLastLoading = false
} }
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
@@ -576,7 +606,6 @@ class Database {
contentResolver: ContentResolver, contentResolver: ContentResolver,
cacheDirectory: File, cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean, isRAMSufficient: (memoryWanted: Long) -> Boolean,
tempCipherKey: LoadedKey,
fixDuplicateUUID: Boolean, fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
@@ -597,41 +626,114 @@ class Database {
// Read database stream for the first time // Read database stream for the first time
readDatabaseStream(contentResolver, uri, readDatabaseStream(contentResolver, uri,
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDB(cacheDirectory, isRAMSufficient) val databaseKDB = DatabaseKDB().apply {
binaryCache.cacheDirectory = cacheDirectory
changeDuplicateId = fixDuplicateUUID
}
DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
mainCredential.masterPassword, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
tempCipherKey, progressTaskUpdater)
progressTaskUpdater, databaseKDB
fixDuplicateUUID)
}, },
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDBX(cacheDirectory, isRAMSufficient) val databaseKDBX = DatabaseKDBX().apply {
.openDatabase(databaseInputStream, binaryCache.cacheDirectory = cacheDirectory
changeDuplicateId = fixDuplicateUUID
}
DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream,
mainCredential.masterPassword, mainCredential.masterPassword,
keyFileInputStream, keyFileInputStream,
tempCipherKey, progressTaskUpdater)
progressTaskUpdater, }
fixDuplicateUUID) databaseKDBX
} }
) )
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e) throw FileNotFoundDatabaseException("Unable to load the keyfile")
throw FileNotFoundDatabaseException()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} finally { } finally {
keyFileInputStream?.close() 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) @Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver, fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean, isRAMSufficient: (memoryWanted: Long) -> Boolean,
tempCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?) { progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI // Retrieve the stream from the old database URI
@@ -639,31 +741,41 @@ class Database {
fileUri?.let { oldDatabaseUri -> fileUri?.let { oldDatabaseUri ->
readDatabaseStream(contentResolver, oldDatabaseUri, readDatabaseStream(contentResolver, oldDatabaseUri,
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDB(cacheDirectory, isRAMSufficient) val databaseKDB = DatabaseKDB()
mDatabaseKDB?.let {
databaseKDB.binaryCache = it.binaryCache
}
DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
masterKey, masterKey,
tempCipherKey,
progressTaskUpdater) progressTaskUpdater)
databaseKDB
}, },
{ databaseInputStream -> { databaseInputStream ->
DatabaseInputKDBX(cacheDirectory, isRAMSufficient) val databaseKDBX = DatabaseKDBX()
.openDatabase(databaseInputStream, mDatabaseKDBX?.let {
databaseKDBX.binaryCache = it.binaryCache
}
DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream,
masterKey, masterKey,
tempCipherKey,
progressTaskUpdater) progressTaskUpdater)
} }
databaseKDBX
}
) )
} ?: run { } ?: run {
Log.e(TAG, "Database URI is null, database cannot be reloaded") throw IODatabaseException("Database URI is null, database cannot be reloaded")
throw IODatabaseException()
} }
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e) throw FileNotFoundDatabaseException("Unable to load the keyfile")
throw FileNotFoundDatabaseException()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} finally {
dataModifiedSinceLastLoading = false
} }
} }
@@ -703,7 +815,7 @@ class Database {
val attachmentPool: AttachmentPool val attachmentPool: AttachmentPool
get() { get() {
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache) return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool()
} }
val allowMultipleAttachments: Boolean val allowMultipleAttachments: Boolean
@@ -716,8 +828,8 @@ class Database {
} }
fun buildNewBinaryAttachment(): BinaryData? { fun buildNewBinaryAttachment(): BinaryData? {
return mDatabaseKDB?.buildNewAttachment() return mDatabaseKDB?.buildNewBinaryAttachment()
?: mDatabaseKDBX?.buildNewAttachment( false, ?: mDatabaseKDBX?.buildNewBinaryAttachment( false,
compressionForNewEntry(), compressionForNewEntry(),
false) false)
} }
@@ -731,6 +843,7 @@ class Database {
fun removeUnlinkedAttachments() { fun removeUnlinkedAttachments() {
// No check in database KDB because unique attachment by entry // No check in database KDB because unique attachment by entry
mDatabaseKDBX?.removeUnlinkedAttachments(true) mDatabaseKDBX?.removeUnlinkedAttachments(true)
dataModifiedSinceLastLoading = true
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
@@ -791,16 +904,25 @@ class Database {
} }
} }
this.fileUri = uri this.fileUri = uri
this.dataModifiedSinceLastLoading = false
} }
fun clear(filesDirectory: File? = null) { fun clearIndexesAndBinaries(filesDirectory: File? = null) {
binaryCache.clear() this.mDatabaseKDB?.clearIndexes()
iconsManager.clearCache() this.mDatabaseKDBX?.clearIndexes()
this.mDatabaseKDB?.clearIconsCache()
this.mDatabaseKDBX?.clearIconsCache()
this.mDatabaseKDB?.clearAttachmentsCache()
this.mDatabaseKDBX?.clearAttachmentsCache()
this.mDatabaseKDB?.clearBinaries()
this.mDatabaseKDBX?.clearBinaries()
iconDrawableFactory.clearCache() iconDrawableFactory.clearCache()
// Delete the cache of the database if present
mDatabaseKDB?.clearCache() // delete all the files in the temp dir if allowed
mDatabaseKDBX?.clearCache()
// In all cases, delete all the files in the temp dir
try { try {
filesDirectory?.let { directory -> filesDirectory?.let { directory ->
cleanDirectory(directory) cleanDirectory(directory)
@@ -811,7 +933,7 @@ class Database {
} }
fun clearAndClose(context: Context? = null) { fun clearAndClose(context: Context? = null) {
clear(context?.let { UriUtil.getBinaryDir(context) }) clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) })
this.mDatabaseKDB = null this.mDatabaseKDB = null
this.mDatabaseKDBX = null this.mDatabaseKDBX = null
this.fileUri = null this.fileUri = null
@@ -838,9 +960,10 @@ class Database {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream) mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream) mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.keyLastChanged = DateInstant()
} }
fun rootCanContainsEntry(): Boolean { fun rootCanContainsEntry(): Boolean {
@@ -848,6 +971,7 @@ class Database {
} }
fun createEntry(): Entry? { fun createEntry(): Entry? {
dataModifiedSinceLastLoading = true
mDatabaseKDB?.let { database -> mDatabaseKDB?.let { database ->
return Entry(database.createEntry()).apply { return Entry(database.createEntry()).apply {
nodeId = database.newEntryId() nodeId = database.newEntryId()
@@ -863,6 +987,7 @@ class Database {
} }
fun createGroup(): Group? { fun createGroup(): Group? {
dataModifiedSinceLastLoading = true
mDatabaseKDB?.let { database -> mDatabaseKDB?.let { database ->
return Group(database.createGroup()).apply { return Group(database.createGroup()).apply {
setNodeId(database.newGroupId()) setNodeId(database.newGroupId())
@@ -900,6 +1025,7 @@ class Database {
} }
fun addEntryTo(entry: Entry, parent: Group) { fun addEntryTo(entry: Entry, parent: Group) {
dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB -> entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB) mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
} }
@@ -910,6 +1036,7 @@ class Database {
} }
fun updateEntry(entry: Entry) { fun updateEntry(entry: Entry) {
dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB -> entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.updateEntry(entryKDB) mDatabaseKDB?.updateEntry(entryKDB)
} }
@@ -919,6 +1046,7 @@ class Database {
} }
fun removeEntryFrom(entry: Entry, parent: Group) { fun removeEntryFrom(entry: Entry, parent: Group) {
dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB -> entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB) mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
} }
@@ -929,6 +1057,7 @@ class Database {
} }
fun addGroupTo(group: Group, parent: Group) { fun addGroupTo(group: Group, parent: Group) {
dataModifiedSinceLastLoading = true
group.groupKDB?.let { groupKDB -> group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB) mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
} }
@@ -939,6 +1068,7 @@ class Database {
} }
fun updateGroup(group: Group) { fun updateGroup(group: Group) {
dataModifiedSinceLastLoading = true
group.groupKDB?.let { entryKDB -> group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.updateGroup(entryKDB) mDatabaseKDB?.updateGroup(entryKDB)
} }
@@ -948,6 +1078,7 @@ class Database {
} }
fun removeGroupFrom(group: Group, parent: Group) { fun removeGroupFrom(group: Group, parent: Group) {
dataModifiedSinceLastLoading = true
group.groupKDB?.let { groupKDB -> group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB) mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
} }
@@ -986,12 +1117,17 @@ class Database {
} }
fun deleteEntry(entry: Entry) { fun deleteEntry(entry: Entry) {
dataModifiedSinceLastLoading = true
entry.entryKDBX?.id?.let { entryId ->
mDatabaseKDBX?.addDeletedObject(entryId)
}
entry.parent?.let { entry.parent?.let {
removeEntryFrom(entry, it) removeEntryFrom(entry, it)
} }
} }
fun deleteGroup(group: Group) { fun deleteGroup(group: Group) {
dataModifiedSinceLastLoading = true
group.doForEachChildAndForIt( group.doForEachChildAndForIt(
object : NodeHandler<Entry>() { object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
@@ -1001,6 +1137,9 @@ class Database {
}, },
object : NodeHandler<Group>() { object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean { override fun operate(node: Group): Boolean {
node.groupKDBX?.id?.let { groupId ->
mDatabaseKDBX?.addDeletedObject(groupId)
}
node.parent?.let { node.parent?.let {
removeGroupFrom(node, it) 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) { fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureBackupExists() mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources) mDatabaseKDBX?.ensureRecycleBinExists(resources)
@@ -1055,47 +1176,41 @@ class Database {
} }
fun recycle(entry: Entry, resources: Resources) { fun recycle(entry: Entry, resources: Resources) {
entry.entryKDB?.let { ensureRecycleBinExists(resources)
mDatabaseKDB?.recycle(it) entry.parent?.let { parent ->
removeEntryFrom(entry, parent)
} }
entry.entryKDBX?.let { recycleBin?.let {
mDatabaseKDBX?.recycle(it, resources) addEntryTo(entry, it)
} }
entry.afterAssignNewParent()
} }
fun recycle(group: Group, resources: Resources) { fun recycle(group: Group, resources: Resources) {
group.groupKDB?.let { ensureRecycleBinExists(resources)
mDatabaseKDB?.recycle(it) group.parent?.let { parent ->
removeGroupFrom(group, parent)
} }
group.groupKDBX?.let { recycleBin?.let {
mDatabaseKDBX?.recycle(it, resources) addGroupTo(group, it)
} }
group.afterAssignNewParent()
} }
fun undoRecycle(entry: Entry, parent: Group) { fun undoRecycle(entry: Entry, parent: Group) {
entry.entryKDB?.let { entryKDB -> recycleBin?.let { it ->
parent.groupKDB?.let { parentKDB -> removeEntryFrom(entry, it)
mDatabaseKDB?.undoRecycle(entryKDB, parentKDB)
}
}
entry.entryKDBX?.let { entryKDBX ->
parent.groupKDBX?.let { parentKDBX ->
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
}
} }
addEntryTo(entry, parent)
entry.afterAssignNewParent()
} }
fun undoRecycle(group: Group, parent: Group) { fun undoRecycle(group: Group, parent: Group) {
group.groupKDB?.let { groupKDB -> recycleBin?.let {
parent.groupKDB?.let { parentKDB -> removeGroupFrom(group, it)
mDatabaseKDB?.undoRecycle(groupKDB, parentKDB)
}
}
group.groupKDBX?.let { entryKDBX ->
parent.groupKDBX?.let { parentKDBX ->
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
}
} }
addGroupTo(group, parent)
group.afterAssignNewParent()
} }
fun startManageEntry(entry: Entry?) { fun startManageEntry(entry: Entry?) {

View File

@@ -28,29 +28,18 @@ import java.util.*
class DeletedObject : Parcelable { class DeletedObject : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO var uuid: UUID = DatabaseVersioned.UUID_ZERO
private var mDeletionTime: DateInstant? = null var deletionTime: DateInstant = DateInstant()
constructor() constructor()
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) { constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
this.uuid = uuid this.uuid = uuid
this.mDeletionTime = deletionTime this.deletionTime = deletionTime
} }
constructor(parcel: Parcel) { constructor(parcel: Parcel) {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
}
fun getDeletionTime(): DateInstant {
if (mDeletionTime == null) {
mDeletionTime = DateInstant(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: DateInstant) {
this.mDeletionTime = deletionTime
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ParcelUuid(uuid), flags) parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeParcelable(mDeletionTime, flags) parcel.writeParcelable(deletionTime, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {

View File

@@ -19,7 +19,7 @@
*/ */
package com.kunzisoft.keepass.database.element.binary package com.kunzisoft.keepass.database.element.binary
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) { class AttachmentPool : BinaryPool<Int>() {
/** /**
* Utility method to find an unused key in the pool * Utility method to find an unused key in the pool

View File

@@ -23,7 +23,7 @@ import android.util.Log
import java.io.IOException import java.io.IOException
import kotlin.math.abs import kotlin.math.abs
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) { abstract class BinaryPool<T> {
protected val pool = LinkedHashMap<T, BinaryData>() protected val pool = LinkedHashMap<T, BinaryData>()
@@ -225,9 +225,6 @@ abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
@Throws(IOException::class) @Throws(IOException::class)
fun clear() { fun clear() {
doForEachBinary { _, binary ->
binary.clear(mBinaryCache)
}
pool.clear() pool.clear()
} }

View File

@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import java.util.* import java.util.*
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) { class CustomIconPool : BinaryPool<UUID>() {
private val customIcons = HashMap<UUID, IconImageCustom>() private val customIcons = HashMap<UUID, IconImageCustom>()
fun put(key: UUID? = null, fun put(key: UUID? = null,
name: String, name: String,
lastModificationTime: DateInstant?, lastModificationTime: DateInstant?,
smallSize: Boolean, builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) { result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = super.put(key) { uniqueBinaryId -> val keyBinary = super.put(key, builder)
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}
val uuid = keyBinary.keys.first() val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime) val customIcon = IconImageCustom(uuid, name, lastModificationTime)
customIcons[uuid] = customIcon customIcons[uuid] = customIcon

View File

@@ -34,11 +34,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() { class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
private var kdfListV3: MutableList<KdfEngine> = ArrayList() override var encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
EncryptionAlgorithm.AESRijndael,
EncryptionAlgorithm.Twofish
)
override val kdfEngine: KdfEngine
get() = kdfAvailableList[0]
override val kdfAvailableList: List<KdfEngine> = listOf(
KdfFactory.aesKdf
)
override val passwordEncoding: String
get() = "ISO-8859-1"
override var numberKeyEncryptionRounds = 300L
override val version: String override val version: String
get() = "V1" get() = "V1"
@@ -48,7 +64,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
rootGroup = createGroup().apply { rootGroup = createGroup().apply {
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID) icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
} }
kdfListV3.add(KdfFactory.aesKdf)
} }
val backupGroup: GroupKDB? val backupGroup: GroupKDB?
@@ -65,28 +80,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
var color: Int? = null var color: Int? = null
override val kdfEngine: KdfEngine
get() = kdfListV3[0]
override val kdfAvailableList: List<KdfEngine>
get() = kdfListV3
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
get() {
val list = ArrayList<EncryptionAlgorithm>()
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 * Generates an unused random tree id
@@ -212,29 +205,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return true return true
} }
fun recycle(group: GroupKDB) { fun buildNewBinaryAttachment(): BinaryData {
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 {
// Generate an unique new file // Generate an unique new file
return attachmentPool.put { uniqueBinaryId -> return attachmentPool.put { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, false) binaryCache.getBinaryData(uniqueBinaryId, false)

View File

@@ -25,8 +25,6 @@ import android.util.Log
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.action.node.NodeHandler 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.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.VariantDictionary import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine 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.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard 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.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible 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_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
@@ -66,6 +64,7 @@ import javax.crypto.Mac
import javax.xml.XMLConstants import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min import kotlin.math.min
@@ -73,27 +72,58 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var hmacKey: ByteArray? = null var hmacKey: ByteArray? = null
private set private set
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
private var dataEngine: CipherEngine = AesEngine() override var encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
var compressionAlgorithm = CompressionAlgorithm.GZip
fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
}
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = 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<KdfEngine> = listOf(
KdfFactory.aesKdf,
KdfFactory.argon2dKdf,
KdfFactory.argon2idKdf
)
var kdfParameters: KdfParameters? = null var kdfParameters: KdfParameters? = null
private var kdfList: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0 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 mFieldReferenceEngine = FieldReferencesEngine(this)
private val mTemplateEngine = TemplateEngineCompatible(this) private val mTemplateEngine = TemplateEngineCompatible(this)
var kdbxVersion = UnsignedInt(0)
var name = "" var name = ""
var nameChanged = DateInstant() var nameChanged = DateInstant()
// TODO change setting date
var settingsChanged = DateInstant()
var description = "" var description = ""
var descriptionChanged = DateInstant() var descriptionChanged = DateInstant()
var defaultUserName = "" var defaultUserName = ""
var defaultUserNameChanged = DateInstant() var defaultUserNameChanged = DateInstant()
var settingsChanged = DateInstant()
// TODO last change date
var keyLastChanged = DateInstant() var keyLastChanged = DateInstant()
var keyChangeRecDays: Long = -1 var keyChangeRecDays: Long = -1
var keyChangeForceDays: Long = 1 var keyChangeForceDays: Long = 1
@@ -115,17 +145,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastSelectedGroupUUID = UUID_ZERO var lastSelectedGroupUUID = UUID_ZERO
var lastTopVisibleGroupUUID = UUID_ZERO var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig() var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>() val deletedObjects = HashSet<DeletedObject>()
var publicCustomData = VariantDictionary()
val customData = CustomData() val customData = CustomData()
var localizedAppName = "KeePassDX" var localizedAppName = "KeePassDX"
init {
kdfList.add(KdfFactory.aesKdf)
kdfList.add(KdfFactory.argon2dKdf)
kdfList.add(KdfFactory.argon2idKdf)
}
constructor() constructor()
/** /**
@@ -148,6 +174,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
} }
var kdbxVersion = UnsignedInt(0)
override val version: String override val version: String
get() { get() {
val kdbxStringVersion = when(kdbxVersion) { val kdbxStringVersion = when(kdbxVersion) {
@@ -159,38 +187,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return "V2 - KDBX$kdbxStringVersion" return "V2 - KDBX$kdbxStringVersion"
} }
override val kdfEngine: KdfEngine? val availableCompressionAlgorithms: List<CompressionAlgorithm> = listOf(
get() = try { CompressionAlgorithm.None,
getEngineKDBX4(kdfParameters) CompressionAlgorithm.GZip
} catch (unknownKDF: UnknownKDF) { )
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
null
}
override val kdfAvailableList: List<KdfEngine>
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<CompressionAlgorithm>
get() {
val list = ArrayList<CompressionAlgorithm>()
list.add(CompressionAlgorithm.None)
list.add(CompressionAlgorithm.GZip)
return list
}
fun changeBinaryCompression(oldCompression: CompressionAlgorithm, fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) { newCompression: CompressionAlgorithm) {
@@ -245,15 +245,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
} }
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
get() {
val list = ArrayList<EncryptionAlgorithm>()
list.add(EncryptionAlgorithm.AESRijndael)
list.add(EncryptionAlgorithm.Twofish)
list.add(EncryptionAlgorithm.ChaCha20)
return list
}
override var numberKeyEncryptionRounds: Long override var numberKeyEncryptionRounds: Long
get() { get() {
val kdfEngine = kdfEngine val kdfEngine = kdfEngine
@@ -305,7 +296,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Retrieve recycle bin in index // Retrieve recycle bin in index
val recycleBin: GroupKDBX? val recycleBin: GroupKDBX?
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID) get() = getGroupByUUID(recycleBinUUID)
val lastSelectedGroup: GroupKDBX? val lastSelectedGroup: GroupKDBX?
get() = getGroupByUUID(lastSelectedGroupUUID) get() = getGroupByUUID(lastSelectedGroupUUID)
@@ -313,17 +304,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val lastTopVisibleGroup: GroupKDBX? val lastTopVisibleGroup: GroupKDBX?
get() = getGroupByUUID(lastTopVisibleGroupUUID) get() = getGroupByUUID(lastTopVisibleGroupUUID)
fun setDataEngine(dataEngine: CipherEngine) {
this.dataEngine = dataEngine
}
override fun getStandardIcon(iconId: Int): IconImageStandard { override fun getStandardIcon(iconId: Int): IconImageStandard {
return this.iconsManager.getIcon(iconId) return this.iconsManager.getIcon(iconId)
} }
fun buildNewCustomIcon(customIconId: UUID? = null, fun buildNewCustomIcon(customIconId: UUID? = null,
result: (IconImageCustom, BinaryData?) -> Unit) { 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, fun addCustomIcon(customIconId: UUID? = null,
@@ -331,14 +319,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
lastModificationTime: DateInstant?, lastModificationTime: DateInstant?,
smallSize: Boolean, smallSize: Boolean,
result: (IconImageCustom, BinaryData?) -> Unit) { 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 { fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
return iconsManager.isCustomIconBinaryDuplicate(binary) return iconsManager.isCustomIconBinaryDuplicate(binary)
} }
fun getCustomIcon(iconUuid: UUID): IconImageCustom { fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
return this.iconsManager.getIcon(iconUuid) return this.iconsManager.getIcon(iconUuid)
} }
@@ -355,7 +350,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val templatesGroup = firstGroupWithValidName val templatesGroup = firstGroupWithValidName
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName) ?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
} else { } else {
removeTemplatesGroup() removeTemplatesGroup()
} }
@@ -363,7 +357,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun removeTemplatesGroup() { fun removeTemplatesGroup() {
entryTemplatesGroup = UUID_ZERO entryTemplatesGroup = UUID_ZERO
entryTemplatesGroupChanged = DateInstant()
mTemplateEngine.clearCache() mTemplateEngine.clearCache()
} }
@@ -475,10 +468,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
@Throws(IOException::class) @Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray) { fun makeFinalKey(masterSeed: ByteArray) {
kdfEngine?.let { keyDerivationFunctionEngine ->
kdfParameters?.let { keyDerivationFunctionParameters -> kdfParameters?.let { keyDerivationFunctionParameters ->
val kdfEngine = getEngineKDBX4(keyDerivationFunctionParameters)
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters) var transformedMasterKey =
keyDerivationFunctionEngine.transform(masterKey, keyDerivationFunctionParameters)
if (transformedMasterKey.size != 32) { if (transformedMasterKey.size != 32) {
transformedMasterKey = HashManager.hashSha256(transformedMasterKey) transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
} }
@@ -486,7 +480,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val cmpKey = ByteArray(65) val cmpKey = ByteArray(65)
System.arraycopy(masterSeed, 0, cmpKey, 0, 32) System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32) System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
finalKey = resizeKey(cmpKey, dataEngine.keyLength()) finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
val messageDigest: MessageDigest val messageDigest: MessageDigest
try { try {
@@ -500,6 +494,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
} }
} }
}
private fun resizeKey(inBytes: ByteArray, cbOut: Int): ByteArray { private fun resizeKey(inBytes: ByteArray, cbOut: Int): ByteArray {
if (cbOut == 0) return ByteArray(0) if (cbOut == 0) return ByteArray(0)
@@ -724,14 +719,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
firstGroupWithValidName firstGroupWithValidName
} }
recycleBinUUID = recycleBinGroup.id recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime recycleBinChanged = DateInstant()
} }
} }
fun removeRecycleBin() { fun removeRecycleBin() {
if (recycleBin != null) { if (recycleBin != null) {
recycleBinUUID = UUID_ZERO recycleBinUUID = UUID_ZERO
recycleBinChanged = DateInstant()
} }
} }
@@ -753,38 +747,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return false return false
} }
fun recycle(group: GroupKDBX, resources: Resources) { fun getDeletedObject(nodeId: NodeId<UUID>): DeletedObject? {
ensureRecycleBinExists(resources) return deletedObjects.find { it.uuid == nodeId.id }
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<DeletedObject> {
return deletedObjects
} }
fun addDeletedObject(deletedObject: DeletedObject) { fun addDeletedObject(deletedObject: DeletedObject) {
this.deletedObjects.add(deletedObject) this.deletedObjects.add(deletedObject)
} }
fun addDeletedObject(objectId: UUID) {
addDeletedObject(DeletedObject(objectId))
}
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) { override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
super.addEntryTo(newEntry, parent) super.addEntryTo(newEntry, parent)
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
@@ -797,20 +771,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) { override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent) super.removeEntryFrom(entryToRemove, parent)
deletedObjects.add(DeletedObject(entryToRemove.id))
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
} }
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
super.undoDeleteEntryFrom(entry, origParent)
deletedObjects.remove(DeletedObject(entry.id))
}
fun containsPublicCustomData(): Boolean { fun containsPublicCustomData(): Boolean {
return publicCustomData.size() > 0 return publicCustomData.size() > 0
} }
fun buildNewAttachment(smallSize: Boolean, fun buildNewBinaryAttachment(smallSize: Boolean,
compression: Boolean, compression: Boolean,
protection: Boolean, protection: Boolean,
binaryPoolId: Int? = null): BinaryData { binaryPoolId: Int? = null): BinaryData {
@@ -830,6 +798,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) { private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
// TODO check in icon pool
// Build binaries to remove with all binaries known // Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryData>() val binariesToRemove = ArrayList<BinaryData>()
if (binaries.isEmpty()) { if (binaries.isEmpty()) {
@@ -866,11 +835,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return super.validatePasswordEncoding(password, containsKeyFile) return super.validatePasswordEncoding(password, containsKeyFile)
} }
override fun clearCache() { override fun clearIndexes() {
try { try {
super.clearCache() super.clearIndexes()
mFieldReferenceEngine.clear() mFieldReferenceEngine.clear()
attachmentPool.clear()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e) Log.e(TAG, "Unable to clear cache", e)
} }

View File

@@ -19,8 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.util.Log
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.binary.BinaryCache
import com.kunzisoft.keepass.database.element.entry.EntryVersioned import com.kunzisoft.keepass.database.element.entry.EntryVersioned
@@ -44,47 +46,37 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry> Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> { > {
// Algorithm used to encrypt the database // Algorithm used to encrypt the database
protected var algorithm: EncryptionAlgorithm? = null abstract var encryptionAlgorithm: EncryptionAlgorithm
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine? abstract val kdfEngine: KdfEngine?
abstract val kdfAvailableList: List<KdfEngine>
abstract var numberKeyEncryptionRounds: Long
abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine> protected abstract val passwordEncoding: String
var masterKey = ByteArray(32) var masterKey = ByteArray(32)
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
abstract val version: String
/** /**
* To manage binaries in faster way * To manage binaries in faster way
* Cipher key generated when the database is loaded, and destroyed when the database is closed * Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements * Can be used to temporarily store database elements
*/ */
var binaryCache = BinaryCache() var binaryCache = BinaryCache()
val iconsManager = IconsManager(binaryCache) var iconsManager = IconsManager()
var attachmentPool = AttachmentPool(binaryCache) var attachmentPool = AttachmentPool()
var changeDuplicateId = false var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>() private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>() private var entryIndexes = LinkedHashMap<NodeId<EntryId>, 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<EncryptionAlgorithm>
var rootGroup: Group? = null var rootGroup: Group? = null
set(value) { set(value) {
field = value field = value
@@ -274,7 +266,7 @@ abstract class DatabaseVersioned<
this.entryIndexes.remove(entry.nodeId) this.entryIndexes.remove(entry.nodeId)
} }
open fun clearCache() { open fun clearIndexes() {
this.groupIndexes.clear() this.groupIndexes.clear()
this.entryIndexes.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 // Remove tree from parent tree
parent?.removeChildGroup(groupToRemove) parent?.removeChildGroup(groupToRemove)
removeGroupIndex(groupToRemove) removeGroupIndex(groupToRemove)
@@ -331,15 +323,6 @@ abstract class DatabaseVersioned<
removeEntryIndex(entryToRemove) 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 abstract fun isInRecycleBin(group: Group): Boolean
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean { fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
@@ -350,6 +333,39 @@ abstract class DatabaseVersioned<
return true 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 { companion object {
private const val TAG = "DatabaseVersioned" private const val TAG = "DatabaseVersioned"

View File

@@ -139,8 +139,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeInt(binaryDataId ?: -1) dest.writeInt(binaryDataId ?: -1)
} }
fun updateWith(source: EntryKDB) { fun updateWith(source: EntryKDB,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
title = source.title title = source.title
username = source.username username = source.username
password = source.password password = source.password

View File

@@ -110,8 +110,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
* Update with deep copy of each entry element * Update with deep copy of each entry element
* @param source * @param source
*/ */
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) { fun updateWith(source: EntryKDBX,
super.updateWith(source) copyHistory: Boolean = true,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
customData = CustomData(source.customData) customData = CustomData(source.customData)

View File

@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeInt(groupFlags) dest.writeInt(groupFlags)
} }
fun updateWith(source: GroupKDB) { fun updateWith(source: GroupKDB,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
groupFlags = source.groupFlags groupFlags = source.groupFlags
} }

View File

@@ -102,8 +102,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(ParcelUuid(previousParentGroup), flags) dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
} }
fun updateWith(source: GroupKDBX) { fun updateWith(source: GroupKDBX,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
usageCount = source.usageCount usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged) locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map // Add all custom elements in map

View File

@@ -51,13 +51,16 @@ abstract class GroupVersioned
dest.writeString(titleGroup) dest.writeString(titleGroup)
} }
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) { protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>,
super.updateWith(source) updateParents: Boolean = true) {
super.updateWith(source, updateParents)
titleGroup = source.titleGroup titleGroup = source.titleGroup
if (updateParents) {
removeChildren() removeChildren()
childGroups.addAll(source.childGroups) childGroups.addAll(source.childGroups)
childEntries.addAll(source.childEntries) childEntries.addAll(source.childEntries)
} }
}
override var title: String override var title: String
get() = titleGroup get() = titleGroup

View File

@@ -28,12 +28,12 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
import java.util.* import java.util.*
class IconsManager(binaryCache: BinaryCache) { class IconsManager {
private val standardCache = List(NB_ICONS) { private val standardCache = List(NB_ICONS) {
IconImageStandard(it) IconImageStandard(it)
} }
private val customCache = CustomIconPool(binaryCache) private val customCache = CustomIconPool()
fun getIcon(iconId: Int): IconImageStandard { fun getIcon(iconId: Int): IconImageStandard {
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
* Custom * 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, fun addCustomIcon(key: UUID? = null,
name: String, name: String,
lastModificationTime: DateInstant?, lastModificationTime: DateInstant?,
smallSize: Boolean, builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) { result: (IconImageCustom, BinaryData?) -> Unit) {
customCache.put(key, name, lastModificationTime, smallSize, result) customCache.put(key, name, lastModificationTime, builder, result)
} }
fun getIcon(iconUuid: UUID): IconImageCustom { fun getIcon(iconUuid: UUID): IconImageCustom? {
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid) return customCache.getCustomIcon(iconUuid)
} }
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return customCache.isBinaryDuplicate(binaryData) return customCache.isBinaryDuplicate(binaryData)
} }
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) { fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
val binary = customCache[iconUuid] val binary = customCache[iconUuid]
customCache.remove(iconUuid) customCache.remove(iconUuid)
try { try {
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
/** /**
* Clear the cache of icons * Clear the cache of icons
*/ */
fun clearCache() { fun clear() {
try {
customCache.clear() customCache.clear()
} catch(e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}
} }
companion object { companion object {

View File

@@ -68,9 +68,12 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
return 0 return 0
} }
protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>) { protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>,
updateParents: Boolean = true) {
this.nodeId = copyNodeId(source.nodeId) this.nodeId = copyNodeId(source.nodeId)
if (updateParents) {
this.parent = source.parent this.parent = source.parent
}
this.icon = source.icon this.icon = source.icon
this.creationTime = DateInstant(source.creationTime) this.creationTime = DateInstant(source.creationTime)
this.lastModificationTime = DateInstant(source.lastModificationTime) this.lastModificationTime = DateInstant(source.lastModificationTime)

View File

@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable) constructor(throwable: Throwable) : super(throwable)
} }
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.file_not_found_content override var errorId: Int = R.string.file_not_found_content
constructor() : super() constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.exception
import java.io.IOException
class UnknownKDF : IOException("Unknown key derivation function")

View File

@@ -256,8 +256,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
if (pbId == null || pbId.size != 16) { if (pbId == null || pbId.size != 16) {
throw IOException("Invalid cipher ID.") throw IOException("Invalid cipher ID.")
} }
databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
databaseV4.cipherUuid = bytes16ToUuid(pbId)
} }
private fun setTransformRound(roundsByte: ByteArray) { private fun setTransformRound(roundsByte: ByteArray) {

View File

@@ -21,16 +21,12 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R 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.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.File
import java.io.InputStream import java.io.InputStream
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var mDatabase: D) {
(protected val cacheDirectory: File,
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
private var startTimeKey = System.currentTimeMillis() private var startTimeKey = System.currentTimeMillis()
private var startTimeContent = System.currentTimeMillis() private var startTimeContent = System.currentTimeMillis()
@@ -49,17 +45,13 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyfileInputStream: InputStream?, keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?): D
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): D
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray, masterKey: ByteArray,
loadedCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?): D
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): D
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) { protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)

View File

@@ -24,7 +24,6 @@ import android.graphics.Color
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.DateInstant 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.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -46,21 +45,15 @@ import kotlin.collections.HashMap
/** /**
* Load a KDB database file. * Load a KDB database file.
*/ */
class DatabaseInputKDB(cacheDirectory: File, class DatabaseInputKDB(database: DatabaseKDB)
isRAMSufficient: (memoryWanted: Long) -> Boolean) : DatabaseInput<DatabaseKDB>(database) {
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
private lateinit var mDatabase: DatabaseKDB
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyfileInputStream: InputStream?, keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
progressTaskUpdater: ProgressTaskUpdater?, return openDatabase(databaseInputStream, progressTaskUpdater) {
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream) mDatabase.retrieveMasterKey(password, keyfileInputStream)
} }
} }
@@ -68,11 +61,8 @@ class DatabaseInputKDB(cacheDirectory: File,
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray, masterKey: ByteArray,
loadedCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
progressTaskUpdater: ProgressTaskUpdater?, return openDatabase(databaseInputStream, progressTaskUpdater) {
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey mDatabase.masterKey = masterKey
} }
} }
@@ -80,7 +70,6 @@ class DatabaseInputKDB(cacheDirectory: File,
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream, private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB { assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try { try {
@@ -107,10 +96,6 @@ class DatabaseInputKDB(cacheDirectory: File,
throw VersionDatabaseException() throw VersionDatabaseException()
} }
mDatabase = DatabaseKDB()
mDatabase.binaryCache.cacheDirectory = cacheDirectory
mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke() assignMasterKey?.invoke()
// Select algorithm // Select algorithm
@@ -281,7 +266,7 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> { 0x000E -> {
newEntry?.let { entry -> newEntry?.let { entry ->
if (fieldSize > 0) { if (fieldSize > 0) {
val binaryData = mDatabase.buildNewAttachment() val binaryData = mDatabase.buildNewBinaryAttachment()
entry.putBinary(binaryData, mDatabase.attachmentPool) entry.putBinary(binaryData, mDatabase.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream -> BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
cipherInputStream.readBytes(fieldSize) { buffer -> cipherInputStream.readBytes(fieldSize) { buffer ->
@@ -346,16 +331,16 @@ class DatabaseInputKDB(cacheDirectory: File,
stopContentTimer() stopContentTimer()
} catch (e: LoadDatabaseException) { } catch (e: LoadDatabaseException) {
mDatabase.clearCache() mDatabase.clearAll()
throw e throw e
} catch (e: IOException) { } catch (e: IOException) {
mDatabase.clearCache() mDatabase.clearAll()
throw IODatabaseException(e) throw IODatabaseException(e)
} catch (e: OutOfMemoryError) { } catch (e: OutOfMemoryError) {
mDatabase.clearCache() mDatabase.clearAll()
throw NoMemoryDatabaseException(e) throw NoMemoryDatabaseException(e)
} catch (e: Exception) { } catch (e: Exception) {
mDatabase.clearCache() mDatabase.clearAll()
throw LoadDatabaseException(e) throw LoadDatabaseException(e)
} }

View File

@@ -22,19 +22,17 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import com.kunzisoft.encrypt.StreamCipher 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.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.HmacBlock import com.kunzisoft.keepass.database.crypto.HmacBlock
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.binary.LoadedKey
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX 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.DatabaseKDBX.Companion.BASE_64_FLAG
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX 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.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.ProtectedString 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.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory import org.xmlpull.v1.XmlPullParserFactory
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
@@ -63,12 +60,10 @@ import javax.crypto.CipherInputStream
import javax.crypto.Mac import javax.crypto.Mac
import kotlin.math.min import kotlin.math.min
class DatabaseInputKDBX(cacheDirectory: File, class DatabaseInputKDBX(database: DatabaseKDBX)
isRAMSufficient: (memoryWanted: Long) -> Boolean) : DatabaseInput<DatabaseKDBX>(database) {
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
private var randomStream: StreamCipher? = null private var randomStream: StreamCipher? = null
private lateinit var mDatabase: DatabaseKDBX
private var hashOfHeader: ByteArray? = null private var hashOfHeader: ByteArray? = null
@@ -97,15 +92,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var entryCustomDataKey: String? = null private var entryCustomDataKey: String? = null
private var entryCustomDataValue: 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) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyfileInputStream: InputStream?, keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
progressTaskUpdater: ProgressTaskUpdater?, return openDatabase(databaseInputStream, progressTaskUpdater) {
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.retrieveMasterKey(password, keyfileInputStream) mDatabase.retrieveMasterKey(password, keyfileInputStream)
} }
} }
@@ -113,11 +111,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray, masterKey: ByteArray,
loadedCipherKey: LoadedKey, progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
progressTaskUpdater: ProgressTaskUpdater?, return openDatabase(databaseInputStream, progressTaskUpdater) {
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
mDatabase.masterKey = masterKey mDatabase.masterKey = masterKey
} }
} }
@@ -125,14 +120,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(LoadDatabaseException::class) @Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream, private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX { assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try { try {
startKeyTimer(progressTaskUpdater) startKeyTimer(progressTaskUpdater)
mDatabase = DatabaseKDBX()
mDatabase.binaryCache.cacheDirectory = cacheDirectory
mDatabase.changeDuplicateId = fixDuplicateUUID
val header = DatabaseHeaderKDBX(mDatabase) val header = DatabaseHeaderKDBX(mDatabase)
@@ -148,13 +138,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
stopKeyTimer() stopKeyTimer()
startContentTimer(progressTaskUpdater) startContentTimer(progressTaskUpdater)
val engine: CipherEngine
val cipher: Cipher val cipher: Cipher
try { try {
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine val engine = mDatabase.encryptionAlgorithm.cipherEngine
engine.forcePaddingCompatibility = true engine.forcePaddingCompatibility = true
mDatabase.setDataEngine(engine)
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV) cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
engine.forcePaddingCompatibility = false engine.forcePaddingCompatibility = false
} catch (e: Exception) { } catch (e: Exception) {
@@ -288,7 +275,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
val byteLength = size - 1 val byteLength = size - 1
// No compression at this level // No compression at this level
val protectedBinary = mDatabase.buildNewAttachment( val protectedBinary = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag) isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream -> protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
dataInputStream.readBytes(byteLength) { buffer -> dataInputStream.readBytes(byteLength) { buffer ->
@@ -524,7 +511,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { } 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)) { } else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
ctxGroup?.tags = readTags(xpp) ctxGroup?.tags = readTags(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
@@ -583,7 +571,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt()) ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) { } 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)) { } else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
ctxEntry?.foregroundColor = readString(xpp) ctxEntry?.foregroundColor = readString(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) { } 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)) { KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
ctxDeletedObject?.uuid = readUuid(xpp) ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp)) ctxDeletedObject?.deletionTime = readDateInstant(xpp)
} else { } else {
readUnknown(xpp) readUnknown(xpp)
} }
@@ -1009,7 +998,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
var binaryRetrieve = mDatabase.attachmentPool[id] var binaryRetrieve = mDatabase.attachmentPool[id]
// Create empty binary if not retrieved in pool // Create empty binary if not retrieved in pool
if (binaryRetrieve == null) { if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewAttachment( binaryRetrieve = mDatabase.buildNewBinaryAttachment(
smallSize = false, smallSize = false,
compression = false, compression = false,
protection = false, protection = false,
@@ -1049,7 +1038,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
return null return null
// Build the new binary and compress // Build the new binary and compress
val binaryAttachment = mDatabase.buildNewAttachment( val binaryAttachment = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId) isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
try { try {
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream -> binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->

View File

@@ -25,7 +25,6 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.exception.DatabaseOutputException 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
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.stream.MacOutputStream import com.kunzisoft.keepass.stream.MacOutputStream
@@ -72,7 +71,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2) mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
mos.write4BytesUInt(header.version) 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.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed) writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)

View File

@@ -263,7 +263,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
private fun setDefaultUsername(entryKDB: EntryKDB) { private fun setDefaultUsername(entryKDB: EntryKDB) {
val binaryData = mDatabaseKDB.buildNewAttachment() val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool) entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream -> BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray()) outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
@@ -271,7 +271,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
private fun setDatabaseColor(entryKDB: EntryKDB) { private fun setDatabaseColor(entryKDB: EntryKDB) {
val binaryData = mDatabaseKDB.buildNewAttachment() val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool) entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream -> BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
var reversColor = Color.BLACK var reversColor = Color.BLACK

View File

@@ -26,7 +26,6 @@ import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.crypto.CipherEngine import com.kunzisoft.keepass.database.crypto.CipherEngine
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm 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.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm 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.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.DatabaseOutputException 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
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
@@ -76,7 +74,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
try { try {
try { try {
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine engine = mDatabaseKDBX.encryptionAlgorithm.cipherEngine
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw DatabaseOutputException("No such cipher", e) 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))) writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
} }
writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true) writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged) writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true) writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
@@ -300,13 +299,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (mDatabaseKDBX.kdfParameters == null) { if (mDatabaseKDBX.kdfParameters == null) {
mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters
} mDatabaseKDBX.randomize()
try {
val kdf = mDatabaseKDBX.getEngineKDBX4(mDatabaseKDBX.kdfParameters)
kdf.randomize(mDatabaseKDBX.kdfParameters!!)
} catch (unknownKDF: UnknownKDF) {
Log.e(TAG, "Unable to retrieve header", unknownKDF)
} }
if (header.version.isBefore(FILE_VERSION_40)) { if (header.version.isBefore(FILE_VERSION_40)) {
@@ -591,7 +584,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject) xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid) writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime()) writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject) xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
} }
@@ -617,7 +610,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeDeletedObjects(value: List<DeletedObject>) { private fun writeDeletedObjects(value: Collection<DeletedObject>) {
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects) xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
for (pdo in value) { for (pdo in value) {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<EntryKDB>() {
override fun operate(node: EntryKDB): Boolean {
mergeEntry(rootGroup.nodeId, node, databaseToMerge)
return true
}
},
object : NodeHandler<GroupKDB>() {
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<UUID>, intId: NodeId<Int>): NodeId<UUID> {
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<UUID>, nodeToMerge: EntryKDB, databaseToMerge: DatabaseKDB) {
val entryId: NodeId<UUID> = 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<UUID>, nodeToMerge: GroupKDB, databaseToMerge: DatabaseKDB) {
val groupId: NodeId<Int> = 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<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean {
mergeEntry(node, databaseToMerge)
return true
}
},
object : NodeHandler<GroupKDBX>() {
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<Attachment>()
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)
}
}
}
}
}
}
}

View File

@@ -226,6 +226,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val actionRunnable: ActionRunnable? = when (intentAction) { val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database) ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database)
ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(database)
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database) ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database) ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database)
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
@@ -287,9 +288,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} finally { } finally {
// Save the database info before performing action // Save the database info before performing action
if (intentAction == ACTION_DATABASE_LOAD_TASK) { when (intentAction) {
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> {
saveDatabaseInfo() saveDatabaseInfo()
} }
}
val save = !database.isReadOnly val save = !database.isReadOnly
&& (intentAction == ACTION_DATABASE_SAVE && (intentAction == ACTION_DATABASE_SAVE
|| intent?.getBooleanExtra(SAVE_DATABASE_KEY, false) == true) || intent?.getBooleanExtra(SAVE_DATABASE_KEY, false) == true)
@@ -331,6 +336,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return when (intentAction) { return when (intentAction) {
ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK, ACTION_DATABASE_RELOAD_TASK,
null -> { null -> {
START_STICKY START_STICKY
@@ -367,6 +373,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
when (intentAction) { when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database ACTION_DATABASE_SAVE -> R.string.saving_database
else -> { else -> {
@@ -378,6 +385,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mMessageId = when (intentAction) { mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> null ACTION_DATABASE_RELOAD_TASK -> null
else -> null else -> null
} }
@@ -385,6 +393,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mWarningId = mWarningId =
if (!saveAction if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK || intentAction == ACTION_DATABASE_LOAD_TASK
|| intentAction == ACTION_DATABASE_MERGE_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK) || intentAction == ACTION_DATABASE_RELOAD_TASK)
null null
else 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 { private fun buildDatabaseReloadActionTask(database: Database): ActionRunnable {
return ReloadDatabaseRunnable( return ReloadDatabaseRunnable(
this, this,
@@ -907,6 +927,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK" const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_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_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_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" const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"

View File

@@ -51,6 +51,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
private var mDatabase: Database? = null private var mDatabase: Database? = null
private var mDatabaseReadOnly: Boolean = false private var mDatabaseReadOnly: Boolean = false
private var mMergeDataAllowed: Boolean = false
private var mDatabaseAutoSaveEnabled: Boolean = true private var mDatabaseAutoSaveEnabled: Boolean = true
private var mScreen: Screen? = null private var mScreen: Screen? = null
@@ -115,6 +116,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.saveDatabase(save) mDatabaseViewModel.saveDatabase(save)
} }
private fun mergeDatabase() {
mDatabaseViewModel.mergeDatabase(false)
}
private fun reloadDatabase() { private fun reloadDatabase() {
mDatabaseViewModel.reloadDatabase(false) mDatabaseViewModel.reloadDatabase(false)
} }
@@ -122,6 +127,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database mDatabase = database
mDatabaseReadOnly = database?.isReadOnly == true mDatabaseReadOnly = database?.isReadOnly == true
mMergeDataAllowed = database?.isMergeDataAllowed() == true
mDatabase?.let { mDatabase?.let {
if (it.loaded) { if (it.loaded) {
@@ -649,6 +655,9 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
if (mDatabaseReadOnly) { if (mDatabaseReadOnly) {
menu.findItem(R.id.menu_save_database)?.isVisible = false 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -657,6 +666,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
saveDatabase(!mDatabaseReadOnly) saveDatabase(!mDatabaseReadOnly)
true true
} }
R.id.menu_merge_database -> {
mergeDatabase()
return true
}
R.id.menu_reload_database -> { R.id.menu_reload_database -> {
reloadDatabase() reloadDatabase()
return true return true

View File

@@ -63,12 +63,10 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
super.onDialogClosed(database, positiveResult) super.onDialogClosed(database, positiveResult)
if (positiveResult) { if (positiveResult) {
database?.let { database?.let {
if (algorithmSelected != null) {
val newAlgorithm = algorithmSelected val newAlgorithm = algorithmSelected
val oldAlgorithm = database.encryptionAlgorithm val oldAlgorithm = database.encryptionAlgorithm
if (newAlgorithm != null) {
database.encryptionAlgorithm = newAlgorithm database.encryptionAlgorithm = newAlgorithm
if (oldAlgorithm != null && newAlgorithm != null)
saveEncryption(oldAlgorithm, newAlgorithm) saveEncryption(oldAlgorithm, newAlgorithm)
} }
} }

View File

@@ -21,6 +21,9 @@ class DatabaseViewModel: ViewModel() {
val saveDatabase : LiveData<Boolean> get() = _saveDatabase val saveDatabase : LiveData<Boolean> get() = _saveDatabase
private val _saveDatabase = SingleLiveEvent<Boolean>() private val _saveDatabase = SingleLiveEvent<Boolean>()
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
private val _mergeDatabase = SingleLiveEvent<Boolean>()
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
private val _reloadDatabase = SingleLiveEvent<Boolean>() private val _reloadDatabase = SingleLiveEvent<Boolean>()
@@ -84,6 +87,10 @@ class DatabaseViewModel: ViewModel() {
_saveDatabase.value = save _saveDatabase.value = save
} }
fun mergeDatabase(fixDuplicateUuid: Boolean) {
_mergeDatabase.value = fixDuplicateUuid
}
fun reloadDatabase(fixDuplicateUuid: Boolean) { fun reloadDatabase(fixDuplicateUuid: Boolean) {
_reloadDatabase.value = fixDuplicateUuid _reloadDatabase.value = fixDuplicateUuid
} }

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601754 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844607 8.7519531 19.306641 C 5.4810648 17.911181 3.4461927 14.150571 4.109375 10.648438 C 4.6649663 7.2806968 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801388 19.308004 7.2166096 19.861328 10.574219 C 20.123351 12.069186 19.935388 13.632673 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671735 12.714066 22.12099 8.4920873 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995703 13 2.0644531 z M 9 6 L 7.5859375 7.4140625 L 11 10.828125 L 11 13 L 7 13 L 12 18 L 17 13 L 13 13 L 13 10 L 12.910156 10 L 12.955078 9.9550781 L 9 6 z M 15.541016 6 L 12.769531 8.7695312 L 14.183594 10.183594 L 16.955078 7.4140625 L 15.541016 6 z" />
</vector>

View File

@@ -25,10 +25,16 @@
android:orderInCategory="95" android:orderInCategory="95"
app:iconTint="?attr/colorControlNormal" app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item android:id="@+id/menu_reload_database" <item android:id="@+id/menu_merge_database"
android:icon="@drawable/ic_downloading_white_24dp" android:icon="@drawable/ic_merge_white_24dp"
android:title="@string/menu_reload_database" android:title="@string/menu_merge_database"
android:orderInCategory="96" android:orderInCategory="96"
app:iconTint="?attr/colorControlNormal" app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item android:id="@+id/menu_reload_database"
android:icon="@drawable/ic_downloading_white_24dp"
android:title="@string/menu_reload_database"
android:orderInCategory="97"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" />
</menu> </menu>

View File

@@ -226,8 +226,9 @@
<string name="menu_cancel">Cancel</string> <string name="menu_cancel">Cancel</string>
<string name="menu_hide_password">Hide password</string> <string name="menu_hide_password">Hide password</string>
<string name="menu_lock">Lock database</string> <string name="menu_lock">Lock database</string>
<string name="menu_save_database">Save database</string> <string name="menu_save_database">Save data</string>
<string name="menu_reload_database">Reload database</string> <string name="menu_merge_database">Merge data</string>
<string name="menu_reload_database">Reload data</string>
<string name="menu_open">Open</string> <string name="menu_open">Open</string>
<string name="menu_search">Search</string> <string name="menu_search">Search</string>
<string name="menu_showpass">Show password</string> <string name="menu_showpass">Show password</string>
@@ -326,7 +327,8 @@
<string name="warning_empty_keyfile">It is not recommended to add an empty keyfile.</string> <string name="warning_empty_keyfile">It is not recommended to add an empty keyfile.</string>
<string name="warning_empty_keyfile_explanation">The content of the keyfile should never be changed, and in the best case, should contain randomly generated data.</string> <string name="warning_empty_keyfile_explanation">The content of the keyfile should never be changed, and in the best case, should contain randomly generated data.</string>
<string name="warning_database_info_changed">The information contained in your database file has been modified outside the app.</string> <string name="warning_database_info_changed">The information contained in your database file has been modified outside the app.</string>
<string name="warning_database_info_changed_options">Overwrite the external modifications by saving the database or reload it with the latest changes.</string> <string name="warning_database_info_changed_options">Merge the data, overwrite the external modifications by saving the database or reload the database with the latest changes.</string>
<string name="warning_database_info_reloaded">Reloading the database will delete the locally modified data.</string>
<string name="warning_database_revoked">Access to the file revoked by the file manager, close the database and reopen it from its location.</string> <string name="warning_database_revoked">Access to the file revoked by the file manager, close the database and reopen it from its location.</string>
<string name="warning_exact_alarm">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.</string> <string name="warning_exact_alarm">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.</string>
<string name="permission">Permission</string> <string name="permission">Permission</string>

59
art/merge_database.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg4"
sodipodi:docname="merge_database.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#c8c8c8"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0.28235294"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview6"
showgrid="true"
inkscape:zoom="22.627418"
inkscape:cx="4.5821118"
inkscape:cy="13.022387"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4">
<inkscape:grid
type="xygrid"
id="grid818" />
</sodipodi:namedview>
<path
style="fill:#ffffff"
d="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601754 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844607 8.7519531 19.306641 C 5.4810648 17.911181 3.4461927 14.150571 4.109375 10.648438 C 4.6649663 7.2806968 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801388 19.308004 7.2166096 19.861328 10.574219 C 20.123351 12.069186 19.935388 13.632673 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671735 12.714066 22.12099 8.4920873 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995703 13 2.0644531 z M 9 6 L 7.5859375 7.4140625 L 11 10.828125 L 11 13 L 7 13 L 12 18 L 17 13 L 13 13 L 13 10 L 12.910156 10 L 12.955078 9.9550781 L 9 6 z M 15.541016 6 L 12.769531 8.7695312 L 14.183594 10.183594 L 16.955078 7.4140625 L 15.541016 6 z "
id="path868" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
* Manage data merge #840 #977

View File

@@ -0,0 +1 @@
* Gestion de la fusion des données #840 #977