mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/Merge_Data' into develop #840
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
KeePassDX(3.2.0)
|
||||
* Manage data merge #840 #977
|
||||
|
||||
KeePassDX(3.1.0)
|
||||
* Add breadcrumb
|
||||
* Add path in search results #1148
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 31
|
||||
versionCode = 92
|
||||
versionName = "3.1.0"
|
||||
versionCode = 93
|
||||
versionName = "3.2.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
|
||||
@@ -376,6 +376,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (!mMergeDataAllowed || mDatabaseReadOnly) {
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
@@ -455,6 +458,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
R.id.menu_save_database -> {
|
||||
saveDatabase()
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
reloadDatabase()
|
||||
}
|
||||
|
||||
@@ -970,6 +970,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
if (mDatabaseReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
}
|
||||
if (!mMergeDataAllowed || mDatabaseReadOnly) {
|
||||
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
} else {
|
||||
@@ -1093,6 +1096,10 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
saveDatabase()
|
||||
return true
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
return true
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
reloadDatabase()
|
||||
return true
|
||||
|
||||
@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
protected var mDatabaseReadOnly: Boolean = true
|
||||
protected var mMergeDataAllowed: Boolean = false
|
||||
private var mAutoSaveEnable: Boolean = true
|
||||
|
||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||
@@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveName.observe(this) {
|
||||
@@ -197,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
mIconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
checkRegister()
|
||||
@@ -212,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Reload the current activity
|
||||
if (result.isSuccess) {
|
||||
@@ -254,8 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(false)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun createEntry(newEntry: Entry,
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
@@ -112,7 +113,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
database.getStandardIcon(standardIconId)
|
||||
},
|
||||
{ customIconId ->
|
||||
database.getCustomIcon(customIconId)
|
||||
database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -122,7 +123,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
database.getStandardIcon(standardIconId)
|
||||
},
|
||||
{ customIconId ->
|
||||
database.getCustomIcon(customIconId)
|
||||
database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
|
||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
} catch (e: Exception) {
|
||||
erase(mBackupKey)
|
||||
setError(e)
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||
@@ -354,6 +356,13 @@ class DatabaseTaskProvider {
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseMerge(fixDuplicateUuid: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_MERGE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
@@ -361,6 +370,19 @@ class DatabaseTaskProvider {
|
||||
, ACTION_DATABASE_RELOAD_TASK)
|
||||
}
|
||||
|
||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||
if (conditionToAsk) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.warning_database_info_reloaded)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
approved.invoke()
|
||||
}.create().show()
|
||||
} else {
|
||||
approved.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
LoadedKey.generateNewCipherKey(),
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
|
||||
private var tempCipherKey: LoadedKey? = null
|
||||
|
||||
override fun onStartRun() {
|
||||
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
|
||||
// Clear before we load
|
||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
||||
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
|
||||
mDatabase.wasReloaded = true
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.reloadData(context.contentResolver,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
|
||||
progressTaskUpdater)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
} else {
|
||||
tempCipherKey = null
|
||||
mDatabase.clearAndClose(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
|
||||
|
||||
foreachNode@ for(nodeToDelete in mNodesToDelete) {
|
||||
mOldParent = nodeToDelete.parent
|
||||
mOldParent?.touch(modified = false, touchParents = true)
|
||||
nodeToDelete.touch(modified = true, touchParents = true)
|
||||
|
||||
when (nodeToDelete.type) {
|
||||
Type.GROUP -> {
|
||||
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
|
||||
// Remove Node from parent
|
||||
mCanRecycle = database.canRecycle(groupToDelete)
|
||||
if (mCanRecycle) {
|
||||
groupToDelete.touch(modified = false, touchParents = true)
|
||||
database.recycle(groupToDelete, context.resources)
|
||||
groupToDelete.setPreviousParentGroup(mOldParent)
|
||||
groupToDelete.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
database.deleteGroup(groupToDelete)
|
||||
}
|
||||
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
|
||||
// Remove Node from parent
|
||||
mCanRecycle = database.canRecycle(entryToDelete)
|
||||
if (mCanRecycle) {
|
||||
entryToDelete.touch(modified = false, touchParents = true)
|
||||
database.recycle(entryToDelete, context.resources)
|
||||
entryToDelete.setPreviousParentGroup(mOldParent)
|
||||
entryToDelete.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
database.deleteEntry(entryToDelete)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
|
||||
foreachNode@ for(nodeToMove in mNodesToMove) {
|
||||
// Move node in new parent
|
||||
mOldParent = nodeToMove.parent
|
||||
nodeToMove.touch(modified = true, touchParents = true)
|
||||
|
||||
when (nodeToMove.type) {
|
||||
Type.GROUP -> {
|
||||
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
|
||||
// and if not in the current group
|
||||
&& groupToMove != mNewParent
|
||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||
groupToMove.touch(modified = true, touchParents = true)
|
||||
database.moveGroupTo(groupToMove, mNewParent)
|
||||
groupToMove.setPreviousParentGroup(mOldParent)
|
||||
groupToMove.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(MoveGroupDatabaseException())
|
||||
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
|
||||
if (mOldParent != mNewParent
|
||||
// and root can contains entry
|
||||
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
||||
entryToMove.touch(modified = true, touchParents = true)
|
||||
database.moveEntryTo(entryToMove, mNewParent)
|
||||
entryToMove.setPreviousParentGroup(mOldParent)
|
||||
entryToMove.touch(modified = true, touchParents = true)
|
||||
} else {
|
||||
// Only finish thread
|
||||
setError(MoveEntryDatabaseException())
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
@@ -94,6 +94,8 @@ class Database {
|
||||
*/
|
||||
var wasReloaded = false
|
||||
|
||||
var dataModifiedSinceLastLoading = false
|
||||
|
||||
var loadTimestamp: Long? = null
|
||||
private set
|
||||
|
||||
@@ -112,7 +114,7 @@ class Database {
|
||||
|
||||
private val iconsManager: IconsManager
|
||||
get() {
|
||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
|
||||
}
|
||||
|
||||
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
||||
@@ -130,7 +132,7 @@ class Database {
|
||||
return iconsManager.doForEachCustomIcon(action)
|
||||
}
|
||||
|
||||
fun getCustomIcon(iconId: UUID): IconImageCustom {
|
||||
fun getCustomIcon(iconId: UUID): IconImageCustom? {
|
||||
return iconsManager.getIcon(iconId)
|
||||
}
|
||||
|
||||
@@ -144,11 +146,12 @@ class Database {
|
||||
|
||||
fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||
iconDrawableFactory.clearFromCache(customIcon)
|
||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||
iconsManager.removeCustomIcon(customIcon.uuid, binaryCache)
|
||||
mDatabaseKDBX?.addDeletedObject(customIcon.uuid)
|
||||
}
|
||||
|
||||
fun updateCustomIcon(customIcon: IconImageCustom) {
|
||||
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
|
||||
iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon)
|
||||
}
|
||||
|
||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||
@@ -212,6 +215,7 @@ class Database {
|
||||
set(name) {
|
||||
mDatabaseKDBX?.name = name
|
||||
mDatabaseKDBX?.nameChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowDescription: Boolean
|
||||
@@ -224,6 +228,7 @@ class Database {
|
||||
set(description) {
|
||||
mDatabaseKDBX?.description = description
|
||||
mDatabaseKDBX?.descriptionChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var defaultUsername: String
|
||||
@@ -234,6 +239,7 @@ class Database {
|
||||
mDatabaseKDB?.defaultUserName = username
|
||||
mDatabaseKDBX?.defaultUserName = username
|
||||
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var customColor: Int?
|
||||
@@ -253,6 +259,8 @@ class Database {
|
||||
} else {
|
||||
ChromaUtil.getFormattedColorString(value, false)
|
||||
}
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowOTP: Boolean
|
||||
@@ -276,6 +284,8 @@ class Database {
|
||||
value?.let {
|
||||
mDatabaseKDBX?.compressionAlgorithm = it
|
||||
}
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
fun compressionForNewEntry(): Boolean {
|
||||
@@ -292,6 +302,7 @@ class Database {
|
||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val allowNoMasterKey: Boolean
|
||||
@@ -306,14 +317,12 @@ class Database {
|
||||
val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
get() = mDatabaseKDB?.availableEncryptionAlgorithms ?: mDatabaseKDBX?.availableEncryptionAlgorithms ?: ArrayList()
|
||||
|
||||
var encryptionAlgorithm: EncryptionAlgorithm?
|
||||
get() = mDatabaseKDB?.encryptionAlgorithm ?: mDatabaseKDBX?.encryptionAlgorithm
|
||||
set(algorithm) {
|
||||
algorithm?.let {
|
||||
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
||||
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
||||
mDatabaseKDBX?.cipherUuid = algorithm.uuid
|
||||
}
|
||||
var encryptionAlgorithm: EncryptionAlgorithm
|
||||
get() = mDatabaseKDB?.encryptionAlgorithm
|
||||
?: mDatabaseKDBX?.encryptionAlgorithm
|
||||
?: EncryptionAlgorithm.AESRijndael
|
||||
set(value) {
|
||||
mDatabaseKDBX?.encryptionAlgorithm = value
|
||||
}
|
||||
|
||||
val availableKdfEngines: List<KdfEngine>
|
||||
@@ -343,6 +352,8 @@ class Database {
|
||||
set(numberRounds) {
|
||||
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
|
||||
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var memoryUsage: Long
|
||||
@@ -351,12 +362,16 @@ class Database {
|
||||
}
|
||||
set(memory) {
|
||||
mDatabaseKDBX?.memoryUsage = memory
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var parallelism: Long
|
||||
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
||||
set(parallelism) {
|
||||
mDatabaseKDBX?.parallelism = parallelism
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var masterKey: ByteArray
|
||||
@@ -364,6 +379,8 @@ class Database {
|
||||
set(masterKey) {
|
||||
mDatabaseKDB?.masterKey = masterKey
|
||||
mDatabaseKDBX?.masterKey = masterKey
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var rootGroup: Group?
|
||||
@@ -414,6 +431,8 @@ class Database {
|
||||
}
|
||||
set(value) {
|
||||
mDatabaseKDBX?.historyMaxItems = value
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
var historyMaxSize: Long
|
||||
@@ -422,6 +441,8 @@ class Database {
|
||||
}
|
||||
set(value) {
|
||||
mDatabaseKDBX?.historyMaxSize = value
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,15 +463,17 @@ class Database {
|
||||
} else {
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
mDatabaseKDBX?.recycleBinChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val recycleBin: Group?
|
||||
get() {
|
||||
mDatabaseKDB?.backupGroup?.let {
|
||||
return Group(it)
|
||||
return getGroupById(it.nodeId) ?: Group(it)
|
||||
}
|
||||
mDatabaseKDBX?.recycleBin?.let {
|
||||
return Group(it)
|
||||
return getGroupById(it.nodeId) ?: Group(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -460,8 +483,10 @@ class Database {
|
||||
if (group != null) {
|
||||
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
|
||||
} else {
|
||||
mDatabaseKDBX?.removeTemplatesGroup()
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
mDatabaseKDBX?.recycleBinChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -477,6 +502,8 @@ class Database {
|
||||
|
||||
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
|
||||
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
|
||||
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val templatesGroup: Group?
|
||||
@@ -492,8 +519,10 @@ class Database {
|
||||
if (group != null) {
|
||||
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
|
||||
} else {
|
||||
mDatabaseKDBX?.entryTemplatesGroup
|
||||
mDatabaseKDBX?.removeTemplatesGroup()
|
||||
}
|
||||
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val groupNamesNotAllowed: List<String>
|
||||
@@ -520,6 +549,7 @@ class Database {
|
||||
this.fileUri = databaseUri
|
||||
// Set Database state
|
||||
this.loaded = true
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@@ -576,7 +606,6 @@ class Database {
|
||||
contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
tempCipherKey: LoadedKey,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
@@ -597,73 +626,156 @@ class Database {
|
||||
// Read database stream for the first time
|
||||
readDatabaseStream(contentResolver, uri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater,
|
||||
fixDuplicateUUID)
|
||||
val databaseKDB = DatabaseKDB().apply {
|
||||
binaryCache.cacheDirectory = cacheDirectory
|
||||
changeDuplicateId = fixDuplicateUUID
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater)
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater,
|
||||
fixDuplicateUUID)
|
||||
val databaseKDBX = DatabaseKDBX().apply {
|
||||
binaryCache.cacheDirectory = cacheDirectory
|
||||
changeDuplicateId = fixDuplicateUUID
|
||||
}
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.e(TAG, "Unable to load keyfile", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun isMergeDataAllowed(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun mergeData(contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
mDatabaseKDB?.let {
|
||||
throw IODatabaseException("Unable to merge from a database V1")
|
||||
}
|
||||
|
||||
// New database instance to get new changes
|
||||
val databaseToMerge = Database()
|
||||
databaseToMerge.fileUri = this.fileUri
|
||||
|
||||
try {
|
||||
databaseToMerge.fileUri?.let { databaseUri ->
|
||||
|
||||
val databaseKDB = DatabaseKDB()
|
||||
val databaseKDBX = DatabaseKDBX()
|
||||
|
||||
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
)
|
||||
|
||||
mDatabaseKDBX?.let { currentDatabaseKDBX ->
|
||||
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
|
||||
this.isRAMSufficient = isRAMSufficient
|
||||
}
|
||||
databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge ->
|
||||
databaseMerger.merge(databaseKDBToMerge)
|
||||
}
|
||||
databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge ->
|
||||
databaseMerger.merge(databaseKDBXToMerge)
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
databaseToMerge.clearAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun reloadData(contentResolver: ContentResolver,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
tempCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
// Retrieve the stream from the old database URI
|
||||
try {
|
||||
fileUri?.let { oldDatabaseUri ->
|
||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
tempCipherKey,
|
||||
progressTaskUpdater)
|
||||
{ databaseInputStream ->
|
||||
val databaseKDB = DatabaseKDB()
|
||||
mDatabaseKDB?.let {
|
||||
databaseKDB.binaryCache = it.binaryCache
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseKDBX = DatabaseKDBX()
|
||||
mDatabaseKDBX?.let {
|
||||
databaseKDBX.binaryCache = it.binaryCache
|
||||
}
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
)
|
||||
} ?: run {
|
||||
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
||||
throw IODatabaseException()
|
||||
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.e(TAG, "Unable to load keyfile", e)
|
||||
throw FileNotFoundDatabaseException()
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,7 +815,7 @@ class Database {
|
||||
|
||||
val attachmentPool: AttachmentPool
|
||||
get() {
|
||||
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
|
||||
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool()
|
||||
}
|
||||
|
||||
val allowMultipleAttachments: Boolean
|
||||
@@ -716,8 +828,8 @@ class Database {
|
||||
}
|
||||
|
||||
fun buildNewBinaryAttachment(): BinaryData? {
|
||||
return mDatabaseKDB?.buildNewAttachment()
|
||||
?: mDatabaseKDBX?.buildNewAttachment( false,
|
||||
return mDatabaseKDB?.buildNewBinaryAttachment()
|
||||
?: mDatabaseKDBX?.buildNewBinaryAttachment( false,
|
||||
compressionForNewEntry(),
|
||||
false)
|
||||
}
|
||||
@@ -731,6 +843,7 @@ class Database {
|
||||
fun removeUnlinkedAttachments() {
|
||||
// No check in database KDB because unique attachment by entry
|
||||
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
@@ -791,16 +904,25 @@ class Database {
|
||||
}
|
||||
}
|
||||
this.fileUri = uri
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
|
||||
fun clear(filesDirectory: File? = null) {
|
||||
binaryCache.clear()
|
||||
iconsManager.clearCache()
|
||||
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
|
||||
this.mDatabaseKDB?.clearIndexes()
|
||||
this.mDatabaseKDBX?.clearIndexes()
|
||||
|
||||
this.mDatabaseKDB?.clearIconsCache()
|
||||
this.mDatabaseKDBX?.clearIconsCache()
|
||||
|
||||
this.mDatabaseKDB?.clearAttachmentsCache()
|
||||
this.mDatabaseKDBX?.clearAttachmentsCache()
|
||||
|
||||
this.mDatabaseKDB?.clearBinaries()
|
||||
this.mDatabaseKDBX?.clearBinaries()
|
||||
|
||||
iconDrawableFactory.clearCache()
|
||||
// Delete the cache of the database if present
|
||||
mDatabaseKDB?.clearCache()
|
||||
mDatabaseKDBX?.clearCache()
|
||||
// In all cases, delete all the files in the temp dir
|
||||
|
||||
// delete all the files in the temp dir if allowed
|
||||
try {
|
||||
filesDirectory?.let { directory ->
|
||||
cleanDirectory(directory)
|
||||
@@ -811,7 +933,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun clearAndClose(context: Context? = null) {
|
||||
clear(context?.let { UriUtil.getBinaryDir(context) })
|
||||
clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) })
|
||||
this.mDatabaseKDB = null
|
||||
this.mDatabaseKDBX = null
|
||||
this.fileUri = null
|
||||
@@ -838,9 +960,10 @@ class Database {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||
}
|
||||
|
||||
fun rootCanContainsEntry(): Boolean {
|
||||
@@ -848,6 +971,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun createEntry(): Entry? {
|
||||
dataModifiedSinceLastLoading = true
|
||||
mDatabaseKDB?.let { database ->
|
||||
return Entry(database.createEntry()).apply {
|
||||
nodeId = database.newEntryId()
|
||||
@@ -863,6 +987,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun createGroup(): Group? {
|
||||
dataModifiedSinceLastLoading = true
|
||||
mDatabaseKDB?.let { database ->
|
||||
return Group(database.createGroup()).apply {
|
||||
setNodeId(database.newGroupId())
|
||||
@@ -900,6 +1025,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun addEntryTo(entry: Entry, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -910,6 +1036,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun updateEntry(entry: Entry) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.updateEntry(entryKDB)
|
||||
}
|
||||
@@ -919,6 +1046,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun removeEntryFrom(entry: Entry, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -929,6 +1057,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun addGroupTo(group: Group, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.groupKDB?.let { groupKDB ->
|
||||
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -939,6 +1068,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun updateGroup(group: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.groupKDB?.let { entryKDB ->
|
||||
mDatabaseKDB?.updateGroup(entryKDB)
|
||||
}
|
||||
@@ -948,6 +1078,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun removeGroupFrom(group: Group, parent: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.groupKDB?.let { groupKDB ->
|
||||
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
||||
}
|
||||
@@ -986,12 +1117,17 @@ class Database {
|
||||
}
|
||||
|
||||
fun deleteEntry(entry: Entry) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
entry.entryKDBX?.id?.let { entryId ->
|
||||
mDatabaseKDBX?.addDeletedObject(entryId)
|
||||
}
|
||||
entry.parent?.let {
|
||||
removeEntryFrom(entry, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteGroup(group: Group) {
|
||||
dataModifiedSinceLastLoading = true
|
||||
group.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
@@ -1001,6 +1137,9 @@ class Database {
|
||||
},
|
||||
object : NodeHandler<Group>() {
|
||||
override fun operate(node: Group): Boolean {
|
||||
node.groupKDBX?.id?.let { groupId ->
|
||||
mDatabaseKDBX?.addDeletedObject(groupId)
|
||||
}
|
||||
node.parent?.let {
|
||||
removeGroupFrom(node, it)
|
||||
}
|
||||
@@ -1009,24 +1148,6 @@ class Database {
|
||||
})
|
||||
}
|
||||
|
||||
fun undoDeleteEntry(entry: Entry, parent: Group) {
|
||||
entry.entryKDB?.let {
|
||||
mDatabaseKDB?.undoDeleteEntryFrom(it, parent.groupKDB)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
mDatabaseKDBX?.undoDeleteEntryFrom(it, parent.groupKDBX)
|
||||
}
|
||||
}
|
||||
|
||||
fun undoDeleteGroup(group: Group, parent: Group) {
|
||||
group.groupKDB?.let {
|
||||
mDatabaseKDB?.undoDeleteGroupFrom(it, parent.groupKDB)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
mDatabaseKDBX?.undoDeleteGroupFrom(it, parent.groupKDBX)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureRecycleBinExists(resources: Resources) {
|
||||
mDatabaseKDB?.ensureBackupExists()
|
||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||
@@ -1055,47 +1176,41 @@ class Database {
|
||||
}
|
||||
|
||||
fun recycle(entry: Entry, resources: Resources) {
|
||||
entry.entryKDB?.let {
|
||||
mDatabaseKDB?.recycle(it)
|
||||
ensureRecycleBinExists(resources)
|
||||
entry.parent?.let { parent ->
|
||||
removeEntryFrom(entry, parent)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
mDatabaseKDBX?.recycle(it, resources)
|
||||
recycleBin?.let {
|
||||
addEntryTo(entry, it)
|
||||
}
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(group: Group, resources: Resources) {
|
||||
group.groupKDB?.let {
|
||||
mDatabaseKDB?.recycle(it)
|
||||
ensureRecycleBinExists(resources)
|
||||
group.parent?.let { parent ->
|
||||
removeGroupFrom(group, parent)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
mDatabaseKDBX?.recycle(it, resources)
|
||||
recycleBin?.let {
|
||||
addGroupTo(group, it)
|
||||
}
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: Entry, parent: Group) {
|
||||
entry.entryKDB?.let { entryKDB ->
|
||||
parent.groupKDB?.let { parentKDB ->
|
||||
mDatabaseKDB?.undoRecycle(entryKDB, parentKDB)
|
||||
}
|
||||
}
|
||||
entry.entryKDBX?.let { entryKDBX ->
|
||||
parent.groupKDBX?.let { parentKDBX ->
|
||||
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
|
||||
}
|
||||
recycleBin?.let { it ->
|
||||
removeEntryFrom(entry, it)
|
||||
}
|
||||
addEntryTo(entry, parent)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: Group, parent: Group) {
|
||||
group.groupKDB?.let { groupKDB ->
|
||||
parent.groupKDB?.let { parentKDB ->
|
||||
mDatabaseKDB?.undoRecycle(groupKDB, parentKDB)
|
||||
}
|
||||
}
|
||||
group.groupKDBX?.let { entryKDBX ->
|
||||
parent.groupKDBX?.let { parentKDBX ->
|
||||
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
|
||||
}
|
||||
recycleBin?.let {
|
||||
removeGroupFrom(group, it)
|
||||
}
|
||||
addGroupTo(group, parent)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun startManageEntry(entry: Entry?) {
|
||||
|
||||
@@ -28,29 +28,18 @@ import java.util.*
|
||||
class DeletedObject : Parcelable {
|
||||
|
||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||
private var mDeletionTime: DateInstant? = null
|
||||
var deletionTime: DateInstant = DateInstant()
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
|
||||
this.uuid = uuid
|
||||
this.mDeletionTime = deletionTime
|
||||
this.deletionTime = deletionTime
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
|
||||
}
|
||||
|
||||
fun getDeletionTime(): DateInstant {
|
||||
if (mDeletionTime == null) {
|
||||
mDeletionTime = DateInstant(System.currentTimeMillis())
|
||||
}
|
||||
return mDeletionTime!!
|
||||
}
|
||||
|
||||
fun setDeletionTime(deletionTime: DateInstant) {
|
||||
this.mDeletionTime = deletionTime
|
||||
deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||
parcel.writeParcelable(mDeletionTime, flags)
|
||||
parcel.writeParcelable(deletionTime, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ import android.util.Log
|
||||
import java.io.IOException
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
||||
abstract class BinaryPool<T> {
|
||||
|
||||
protected val pool = LinkedHashMap<T, BinaryData>()
|
||||
|
||||
@@ -225,9 +225,6 @@ abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
doForEachBinary { _, binary ->
|
||||
binary.clear(mBinaryCache)
|
||||
}
|
||||
pool.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import java.util.*
|
||||
|
||||
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
||||
class CustomIconPool : BinaryPool<UUID>() {
|
||||
|
||||
private val customIcons = HashMap<UUID, IconImageCustom>()
|
||||
|
||||
fun put(key: UUID? = null,
|
||||
name: String,
|
||||
lastModificationTime: DateInstant?,
|
||||
smallSize: Boolean,
|
||||
builder: (uniqueBinaryId: String) -> BinaryData,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
val keyBinary = super.put(key) { uniqueBinaryId ->
|
||||
// Create a byte array for better performance with small data
|
||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||
}
|
||||
val keyBinary = super.put(key, builder)
|
||||
val uuid = keyBinary.keys.first()
|
||||
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
|
||||
customIcons[uuid] = customIcon
|
||||
|
||||
@@ -34,11 +34,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class DatabaseKDB : DatabaseVersioned<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
|
||||
get() = "V1"
|
||||
@@ -48,7 +64,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
rootGroup = createGroup().apply {
|
||||
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
|
||||
}
|
||||
kdfListV3.add(KdfFactory.aesKdf)
|
||||
}
|
||||
|
||||
val backupGroup: GroupKDB?
|
||||
@@ -65,28 +80,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
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
|
||||
@@ -212,29 +205,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return true
|
||||
}
|
||||
|
||||
fun recycle(group: GroupKDB) {
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, backupGroup)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryKDB) {
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, backupGroup)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: GroupKDB, origParent: GroupKDB) {
|
||||
removeGroupFrom(group, backupGroup)
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: EntryKDB, origParent: GroupKDB) {
|
||||
removeEntryFrom(entry, backupGroup)
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun buildNewAttachment(): BinaryData {
|
||||
fun buildNewBinaryAttachment(): BinaryData {
|
||||
// Generate an unique new file
|
||||
return attachmentPool.put { uniqueBinaryId ->
|
||||
binaryCache.getBinaryData(uniqueBinaryId, false)
|
||||
|
||||
@@ -25,8 +25,6 @@ import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.AesEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
@@ -42,12 +40,12 @@ import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
@@ -66,6 +64,7 @@ import javax.crypto.Mac
|
||||
import javax.xml.XMLConstants
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
import kotlin.collections.HashSet
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@@ -73,27 +72,58 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
var hmacKey: ByteArray? = null
|
||||
private set
|
||||
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
|
||||
private var dataEngine: CipherEngine = AesEngine()
|
||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||
|
||||
override var encryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||
|
||||
fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
|
||||
encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
|
||||
}
|
||||
|
||||
override val availableEncryptionAlgorithms: List<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
|
||||
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
|
||||
fun randomize() {
|
||||
kdfParameters?.let {
|
||||
kdfEngine?.randomize(it)
|
||||
}
|
||||
}
|
||||
|
||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||
|
||||
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||
private val mTemplateEngine = TemplateEngineCompatible(this)
|
||||
|
||||
var kdbxVersion = UnsignedInt(0)
|
||||
var name = ""
|
||||
var nameChanged = DateInstant()
|
||||
// TODO change setting date
|
||||
var settingsChanged = DateInstant()
|
||||
var description = ""
|
||||
var descriptionChanged = DateInstant()
|
||||
var defaultUserName = ""
|
||||
var defaultUserNameChanged = DateInstant()
|
||||
|
||||
// TODO last change date
|
||||
var settingsChanged = DateInstant()
|
||||
var keyLastChanged = DateInstant()
|
||||
var keyChangeRecDays: Long = -1
|
||||
var keyChangeForceDays: Long = 1
|
||||
@@ -115,17 +145,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
var lastSelectedGroupUUID = UUID_ZERO
|
||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||
var memoryProtection = MemoryProtectionConfig()
|
||||
val deletedObjects = ArrayList<DeletedObject>()
|
||||
val deletedObjects = HashSet<DeletedObject>()
|
||||
|
||||
var publicCustomData = VariantDictionary()
|
||||
val customData = CustomData()
|
||||
|
||||
var localizedAppName = "KeePassDX"
|
||||
|
||||
init {
|
||||
kdfList.add(KdfFactory.aesKdf)
|
||||
kdfList.add(KdfFactory.argon2dKdf)
|
||||
kdfList.add(KdfFactory.argon2idKdf)
|
||||
}
|
||||
|
||||
constructor()
|
||||
|
||||
/**
|
||||
@@ -148,6 +174,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
}
|
||||
|
||||
var kdbxVersion = UnsignedInt(0)
|
||||
|
||||
override val version: String
|
||||
get() {
|
||||
val kdbxStringVersion = when(kdbxVersion) {
|
||||
@@ -159,38 +187,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return "V2 - KDBX$kdbxStringVersion"
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = try {
|
||||
getEngineKDBX4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
null
|
||||
}
|
||||
|
||||
override val kdfAvailableList: List<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
|
||||
}
|
||||
val availableCompressionAlgorithms: List<CompressionAlgorithm> = listOf(
|
||||
CompressionAlgorithm.None,
|
||||
CompressionAlgorithm.GZip
|
||||
)
|
||||
|
||||
fun changeBinaryCompression(oldCompression: 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
|
||||
get() {
|
||||
val kdfEngine = kdfEngine
|
||||
@@ -305,7 +296,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
// Retrieve recycle bin in index
|
||||
val recycleBin: GroupKDBX?
|
||||
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID)
|
||||
get() = getGroupByUUID(recycleBinUUID)
|
||||
|
||||
val lastSelectedGroup: GroupKDBX?
|
||||
get() = getGroupByUUID(lastSelectedGroupUUID)
|
||||
@@ -313,17 +304,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val lastTopVisibleGroup: GroupKDBX?
|
||||
get() = getGroupByUUID(lastTopVisibleGroupUUID)
|
||||
|
||||
fun setDataEngine(dataEngine: CipherEngine) {
|
||||
this.dataEngine = dataEngine
|
||||
}
|
||||
|
||||
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||
return this.iconsManager.getIcon(iconId)
|
||||
}
|
||||
|
||||
fun buildNewCustomIcon(customIconId: UUID? = null,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
iconsManager.buildNewCustomIcon(customIconId, result)
|
||||
// Create a binary file for a brand new custom icon
|
||||
addCustomIcon(customIconId, "", null, false, result)
|
||||
}
|
||||
|
||||
fun addCustomIcon(customIconId: UUID? = null,
|
||||
@@ -331,14 +319,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
lastModificationTime: DateInstant?,
|
||||
smallSize: Boolean,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
|
||||
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, { uniqueBinaryId ->
|
||||
// Create a byte array for better performance with small data
|
||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||
}, result)
|
||||
}
|
||||
|
||||
fun removeCustomIcon(iconUuid: UUID) {
|
||||
iconsManager.removeCustomIcon(iconUuid, binaryCache)
|
||||
}
|
||||
|
||||
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||
return iconsManager.isCustomIconBinaryDuplicate(binary)
|
||||
}
|
||||
|
||||
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
|
||||
fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
|
||||
return this.iconsManager.getIcon(iconUuid)
|
||||
}
|
||||
|
||||
@@ -355,7 +350,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val templatesGroup = firstGroupWithValidName
|
||||
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||
entryTemplatesGroup = templatesGroup.id
|
||||
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
||||
} else {
|
||||
removeTemplatesGroup()
|
||||
}
|
||||
@@ -363,7 +357,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
fun removeTemplatesGroup() {
|
||||
entryTemplatesGroup = UUID_ZERO
|
||||
entryTemplatesGroupChanged = DateInstant()
|
||||
mTemplateEngine.clearCache()
|
||||
}
|
||||
|
||||
@@ -475,28 +468,30 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
@Throws(IOException::class)
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
kdfParameters?.let { keyDerivationFunctionParameters ->
|
||||
val kdfEngine = getEngineKDBX4(keyDerivationFunctionParameters)
|
||||
kdfEngine?.let { keyDerivationFunctionEngine ->
|
||||
kdfParameters?.let { keyDerivationFunctionParameters ->
|
||||
|
||||
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||
if (transformedMasterKey.size != 32) {
|
||||
transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
|
||||
}
|
||||
var transformedMasterKey =
|
||||
keyDerivationFunctionEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||
if (transformedMasterKey.size != 32) {
|
||||
transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
|
||||
}
|
||||
|
||||
val cmpKey = ByteArray(65)
|
||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||
finalKey = resizeKey(cmpKey, dataEngine.keyLength())
|
||||
val cmpKey = ByteArray(65)
|
||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||
finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
|
||||
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-512")
|
||||
cmpKey[64] = 1
|
||||
hmacKey = messageDigest.digest(cmpKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("No SHA-512 implementation")
|
||||
} finally {
|
||||
Arrays.fill(cmpKey, 0.toByte())
|
||||
val messageDigest: MessageDigest
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-512")
|
||||
cmpKey[64] = 1
|
||||
hmacKey = messageDigest.digest(cmpKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("No SHA-512 implementation")
|
||||
} finally {
|
||||
Arrays.fill(cmpKey, 0.toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -724,14 +719,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
firstGroupWithValidName
|
||||
}
|
||||
recycleBinUUID = recycleBinGroup.id
|
||||
recycleBinChanged = recycleBinGroup.lastModificationTime
|
||||
recycleBinChanged = DateInstant()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRecycleBin() {
|
||||
if (recycleBin != null) {
|
||||
recycleBinUUID = UUID_ZERO
|
||||
recycleBinChanged = DateInstant()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,38 +747,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return false
|
||||
}
|
||||
|
||||
fun recycle(group: GroupKDBX, resources: Resources) {
|
||||
ensureRecycleBinExists(resources)
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, recycleBin)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryKDBX, resources: Resources) {
|
||||
ensureRecycleBinExists(resources)
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, recycleBin)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: GroupKDBX, origParent: GroupKDBX) {
|
||||
removeGroupFrom(group, recycleBin)
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: EntryKDBX, origParent: GroupKDBX) {
|
||||
removeEntryFrom(entry, recycleBin)
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun getDeletedObjects(): List<DeletedObject> {
|
||||
return deletedObjects
|
||||
fun getDeletedObject(nodeId: NodeId<UUID>): DeletedObject? {
|
||||
return deletedObjects.find { it.uuid == nodeId.id }
|
||||
}
|
||||
|
||||
fun addDeletedObject(deletedObject: DeletedObject) {
|
||||
this.deletedObjects.add(deletedObject)
|
||||
}
|
||||
|
||||
fun addDeletedObject(objectId: UUID) {
|
||||
addDeletedObject(DeletedObject(objectId))
|
||||
}
|
||||
|
||||
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
||||
super.addEntryTo(newEntry, parent)
|
||||
mFieldReferenceEngine.clear()
|
||||
@@ -797,23 +771,17 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
||||
super.removeEntryFrom(entryToRemove, parent)
|
||||
deletedObjects.add(DeletedObject(entryToRemove.id))
|
||||
mFieldReferenceEngine.clear()
|
||||
}
|
||||
|
||||
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
||||
super.undoDeleteEntryFrom(entry, origParent)
|
||||
deletedObjects.remove(DeletedObject(entry.id))
|
||||
}
|
||||
|
||||
fun containsPublicCustomData(): Boolean {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
fun buildNewAttachment(smallSize: Boolean,
|
||||
compression: Boolean,
|
||||
protection: Boolean,
|
||||
binaryPoolId: Int? = null): BinaryData {
|
||||
fun buildNewBinaryAttachment(smallSize: Boolean,
|
||||
compression: Boolean,
|
||||
protection: Boolean,
|
||||
binaryPoolId: Int? = null): BinaryData {
|
||||
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
|
||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
|
||||
}.binary
|
||||
@@ -830,6 +798,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
}
|
||||
|
||||
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
|
||||
// TODO check in icon pool
|
||||
// Build binaries to remove with all binaries known
|
||||
val binariesToRemove = ArrayList<BinaryData>()
|
||||
if (binaries.isEmpty()) {
|
||||
@@ -866,11 +835,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return super.validatePasswordEncoding(password, containsKeyFile)
|
||||
}
|
||||
|
||||
override fun clearCache() {
|
||||
override fun clearIndexes() {
|
||||
try {
|
||||
super.clearCache()
|
||||
super.clearIndexes()
|
||||
mFieldReferenceEngine.clear()
|
||||
attachmentPool.clear()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear cache", e)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||
@@ -44,47 +46,37 @@ abstract class DatabaseVersioned<
|
||||
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
|
||||
> {
|
||||
|
||||
|
||||
// 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 finalKey: ByteArray? = null
|
||||
protected set
|
||||
|
||||
abstract val version: String
|
||||
|
||||
/**
|
||||
* To manage binaries in faster way
|
||||
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||
* Can be used to temporarily store database elements
|
||||
*/
|
||||
var binaryCache = BinaryCache()
|
||||
val iconsManager = IconsManager(binaryCache)
|
||||
var attachmentPool = AttachmentPool(binaryCache)
|
||||
var iconsManager = IconsManager()
|
||||
var attachmentPool = AttachmentPool()
|
||||
|
||||
var changeDuplicateId = false
|
||||
|
||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||
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
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -274,7 +266,7 @@ abstract class DatabaseVersioned<
|
||||
this.entryIndexes.remove(entry.nodeId)
|
||||
}
|
||||
|
||||
open fun clearCache() {
|
||||
open fun clearIndexes() {
|
||||
this.groupIndexes.clear()
|
||||
this.entryIndexes.clear()
|
||||
}
|
||||
@@ -304,7 +296,7 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
open fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
// Remove tree from parent tree
|
||||
parent?.removeChildGroup(groupToRemove)
|
||||
removeGroupIndex(groupToRemove)
|
||||
@@ -331,15 +323,6 @@ abstract class DatabaseVersioned<
|
||||
removeEntryIndex(entryToRemove)
|
||||
}
|
||||
|
||||
// TODO Delete group
|
||||
fun undoDeleteGroupFrom(group: Group, origParent: Group?) {
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
open fun undoDeleteEntryFrom(entry: Entry, origParent: Group?) {
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
abstract fun isInRecycleBin(group: Group): Boolean
|
||||
|
||||
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
|
||||
@@ -350,6 +333,39 @@ abstract class DatabaseVersioned<
|
||||
return true
|
||||
}
|
||||
|
||||
fun clearIconsCache() {
|
||||
iconsManager.doForEachCustomIcon { _, binary ->
|
||||
try {
|
||||
binary.clear(binaryCache)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear icon binary cache", e)
|
||||
}
|
||||
}
|
||||
iconsManager.clear()
|
||||
}
|
||||
|
||||
fun clearAttachmentsCache() {
|
||||
attachmentPool.doForEachBinary { _, binary ->
|
||||
try {
|
||||
binary.clear(binaryCache)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to clear attachment binary cache", e)
|
||||
}
|
||||
}
|
||||
attachmentPool.clear()
|
||||
}
|
||||
|
||||
fun clearBinaries() {
|
||||
binaryCache.clear()
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
clearIndexes()
|
||||
clearIconsCache()
|
||||
clearAttachmentsCache()
|
||||
clearBinaries()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "DatabaseVersioned"
|
||||
|
||||
@@ -139,8 +139,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
dest.writeInt(binaryDataId ?: -1)
|
||||
}
|
||||
|
||||
fun updateWith(source: EntryKDB) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: EntryKDB,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
title = source.title
|
||||
username = source.username
|
||||
password = source.password
|
||||
|
||||
@@ -110,8 +110,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
* Update with deep copy of each entry element
|
||||
* @param source
|
||||
*/
|
||||
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: EntryKDBX,
|
||||
copyHistory: Boolean = true,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
usageCount = source.usageCount
|
||||
locationChanged = DateInstant(source.locationChanged)
|
||||
customData = CustomData(source.customData)
|
||||
|
||||
@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
dest.writeInt(groupFlags)
|
||||
}
|
||||
|
||||
fun updateWith(source: GroupKDB) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: GroupKDB,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
groupFlags = source.groupFlags
|
||||
}
|
||||
|
||||
|
||||
@@ -102,8 +102,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
||||
}
|
||||
|
||||
fun updateWith(source: GroupKDBX) {
|
||||
super.updateWith(source)
|
||||
fun updateWith(source: GroupKDBX,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
usageCount = source.usageCount
|
||||
locationChanged = DateInstant(source.locationChanged)
|
||||
// Add all custom elements in map
|
||||
|
||||
@@ -51,12 +51,15 @@ abstract class GroupVersioned
|
||||
dest.writeString(titleGroup)
|
||||
}
|
||||
|
||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
|
||||
super.updateWith(source)
|
||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>,
|
||||
updateParents: Boolean = true) {
|
||||
super.updateWith(source, updateParents)
|
||||
titleGroup = source.titleGroup
|
||||
removeChildren()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.addAll(source.childEntries)
|
||||
if (updateParents) {
|
||||
removeChildren()
|
||||
childGroups.addAll(source.childGroups)
|
||||
childEntries.addAll(source.childEntries)
|
||||
}
|
||||
}
|
||||
|
||||
override var title: String
|
||||
|
||||
@@ -28,12 +28,12 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
|
||||
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||
import java.util.*
|
||||
|
||||
class IconsManager(binaryCache: BinaryCache) {
|
||||
class IconsManager {
|
||||
|
||||
private val standardCache = List(NB_ICONS) {
|
||||
IconImageStandard(it)
|
||||
}
|
||||
private val customCache = CustomIconPool(binaryCache)
|
||||
private val customCache = CustomIconPool()
|
||||
|
||||
fun getIcon(iconId: Int): IconImageStandard {
|
||||
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
||||
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
|
||||
* Custom
|
||||
*/
|
||||
|
||||
fun buildNewCustomIcon(key: UUID? = null,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
// Create a binary file for a brand new custom icon
|
||||
addCustomIcon(key, "", null, false, result)
|
||||
}
|
||||
|
||||
fun addCustomIcon(key: UUID? = null,
|
||||
name: String,
|
||||
lastModificationTime: DateInstant?,
|
||||
smallSize: Boolean,
|
||||
builder: (uniqueBinaryId: String) -> BinaryData,
|
||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||
customCache.put(key, name, lastModificationTime, smallSize, result)
|
||||
customCache.put(key, name, lastModificationTime, builder, result)
|
||||
}
|
||||
|
||||
fun getIcon(iconUuid: UUID): IconImageCustom {
|
||||
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
|
||||
fun getIcon(iconUuid: UUID): IconImageCustom? {
|
||||
return customCache.getCustomIcon(iconUuid)
|
||||
}
|
||||
|
||||
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||
return customCache.isBinaryDuplicate(binaryData)
|
||||
}
|
||||
|
||||
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) {
|
||||
fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
|
||||
val binary = customCache[iconUuid]
|
||||
customCache.remove(iconUuid)
|
||||
try {
|
||||
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
|
||||
/**
|
||||
* Clear the cache of icons
|
||||
*/
|
||||
fun clearCache() {
|
||||
try {
|
||||
customCache.clear()
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to clear cache", e)
|
||||
}
|
||||
fun clear() {
|
||||
customCache.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -68,9 +68,12 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
||||
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.parent = source.parent
|
||||
if (updateParents) {
|
||||
this.parent = source.parent
|
||||
}
|
||||
this.icon = source.icon
|
||||
this.creationTime = DateInstant(source.creationTime)
|
||||
this.lastModificationTime = DateInstant(source.lastModificationTime)
|
||||
|
||||
@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
}
|
||||
|
||||
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.file_not_found_content
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
constructor(string: String) : super(string)
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -256,8 +256,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
if (pbId == null || pbId.size != 16) {
|
||||
throw IOException("Invalid cipher ID.")
|
||||
}
|
||||
|
||||
databaseV4.cipherUuid = bytes16ToUuid(pbId)
|
||||
databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
|
||||
}
|
||||
|
||||
private fun setTransformRound(roundsByte: ByteArray) {
|
||||
|
||||
@@ -21,16 +21,12 @@ package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
||||
(protected val cacheDirectory: File,
|
||||
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
|
||||
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var mDatabase: D) {
|
||||
|
||||
private var startTimeKey = System.currentTimeMillis()
|
||||
private var startTimeContent = System.currentTimeMillis()
|
||||
@@ -49,17 +45,13 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean = false): D
|
||||
progressTaskUpdater: ProgressTaskUpdater?): D
|
||||
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean = false): D
|
||||
progressTaskUpdater: ProgressTaskUpdater?): D
|
||||
|
||||
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.graphics.Color
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
@@ -46,21 +45,15 @@ import kotlin.collections.HashMap
|
||||
/**
|
||||
* Load a KDB database file.
|
||||
*/
|
||||
class DatabaseInputKDB(cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
||||
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
|
||||
|
||||
private lateinit var mDatabase: DatabaseKDB
|
||||
class DatabaseInputKDB(database: DatabaseKDB)
|
||||
: DatabaseInput<DatabaseKDB>(database) {
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||
}
|
||||
}
|
||||
@@ -68,11 +61,8 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
@@ -80,7 +70,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean,
|
||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
||||
|
||||
try {
|
||||
@@ -107,10 +96,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
throw VersionDatabaseException()
|
||||
}
|
||||
|
||||
mDatabase = DatabaseKDB()
|
||||
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
||||
|
||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||
assignMasterKey?.invoke()
|
||||
|
||||
// Select algorithm
|
||||
@@ -281,7 +266,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
0x000E -> {
|
||||
newEntry?.let { entry ->
|
||||
if (fieldSize > 0) {
|
||||
val binaryData = mDatabase.buildNewAttachment()
|
||||
val binaryData = mDatabase.buildNewBinaryAttachment()
|
||||
entry.putBinary(binaryData, mDatabase.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
|
||||
cipherInputStream.readBytes(fieldSize) { buffer ->
|
||||
@@ -346,16 +331,16 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
stopContentTimer()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
mDatabase.clearCache()
|
||||
mDatabase.clearAll()
|
||||
throw LoadDatabaseException(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,19 +22,17 @@ package com.kunzisoft.keepass.database.file.input
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.StreamCipher
|
||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
@@ -50,7 +48,6 @@ import com.kunzisoft.keepass.utils.*
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
@@ -63,12 +60,10 @@ import javax.crypto.CipherInputStream
|
||||
import javax.crypto.Mac
|
||||
import kotlin.math.min
|
||||
|
||||
class DatabaseInputKDBX(cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
||||
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
|
||||
class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
: DatabaseInput<DatabaseKDBX>(database) {
|
||||
|
||||
private var randomStream: StreamCipher? = null
|
||||
private lateinit var mDatabase: DatabaseKDBX
|
||||
|
||||
private var hashOfHeader: ByteArray? = null
|
||||
|
||||
@@ -97,15 +92,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
private var entryCustomDataKey: String? = null
|
||||
private var entryCustomDataValue: String? = null
|
||||
|
||||
private var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
|
||||
|
||||
fun setMethodToCheckIfRAMIsSufficient(method: (memoryWanted: Long) -> Boolean) {
|
||||
this.isRAMSufficient = method
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||
}
|
||||
}
|
||||
@@ -113,11 +111,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
loadedCipherKey: LoadedKey,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
@@ -125,14 +120,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
fixDuplicateUUID: Boolean,
|
||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
||||
try {
|
||||
startKeyTimer(progressTaskUpdater)
|
||||
mDatabase = DatabaseKDBX()
|
||||
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
||||
|
||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
||||
|
||||
val header = DatabaseHeaderKDBX(mDatabase)
|
||||
|
||||
@@ -148,13 +138,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
stopKeyTimer()
|
||||
startContentTimer(progressTaskUpdater)
|
||||
|
||||
val engine: CipherEngine
|
||||
val cipher: Cipher
|
||||
try {
|
||||
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
|
||||
val engine = mDatabase.encryptionAlgorithm.cipherEngine
|
||||
engine.forcePaddingCompatibility = true
|
||||
mDatabase.setDataEngine(engine)
|
||||
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
|
||||
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
||||
engine.forcePaddingCompatibility = false
|
||||
} catch (e: Exception) {
|
||||
@@ -288,7 +275,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||
val byteLength = size - 1
|
||||
// No compression at this level
|
||||
val protectedBinary = mDatabase.buildNewAttachment(
|
||||
val protectedBinary = mDatabase.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
|
||||
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||
dataInputStream.readBytes(byteLength) { buffer ->
|
||||
@@ -524,7 +511,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
||||
val iconUUID = readUuid(xpp)
|
||||
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
|
||||
ctxGroup?.tags = readTags(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
|
||||
@@ -583,7 +571,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
||||
val iconUUID = readUuid(xpp)
|
||||
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
||||
ctxEntry?.foregroundColor = readString(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
|
||||
@@ -704,7 +693,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||
ctxDeletedObject?.uuid = readUuid(xpp)
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
||||
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp))
|
||||
ctxDeletedObject?.deletionTime = readDateInstant(xpp)
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
@@ -1009,7 +998,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
var binaryRetrieve = mDatabase.attachmentPool[id]
|
||||
// Create empty binary if not retrieved in pool
|
||||
if (binaryRetrieve == null) {
|
||||
binaryRetrieve = mDatabase.buildNewAttachment(
|
||||
binaryRetrieve = mDatabase.buildNewBinaryAttachment(
|
||||
smallSize = false,
|
||||
compression = false,
|
||||
protection = false,
|
||||
@@ -1049,7 +1038,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
return null
|
||||
|
||||
// Build the new binary and compress
|
||||
val binaryAttachment = mDatabase.buildNewAttachment(
|
||||
val binaryAttachment = mDatabase.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
|
||||
try {
|
||||
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.stream.MacOutputStream
|
||||
@@ -72,7 +71,7 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
|
||||
mos.write4BytesUInt(header.version)
|
||||
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.cipherUuid))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.encryptionAlgorithm.uuid))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
private fun setDefaultUsername(entryKDB: EntryKDB) {
|
||||
val binaryData = mDatabaseKDB.buildNewAttachment()
|
||||
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
|
||||
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
|
||||
@@ -271,7 +271,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
private fun setDatabaseColor(entryKDB: EntryKDB) {
|
||||
val binaryData = mDatabaseKDB.buildNewAttachment()
|
||||
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
|
||||
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||
var reversColor = Color.BLACK
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.kunzisoft.encrypt.StreamCipher
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
@@ -39,7 +38,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
@@ -76,7 +74,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
|
||||
try {
|
||||
try {
|
||||
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
|
||||
engine = mDatabaseKDBX.encryptionAlgorithm.cipherEngine
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw DatabaseOutputException("No such cipher", e)
|
||||
}
|
||||
@@ -240,6 +238,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
|
||||
}
|
||||
|
||||
writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
|
||||
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
|
||||
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
|
||||
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
|
||||
@@ -300,13 +299,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
|
||||
if (mDatabaseKDBX.kdfParameters == null) {
|
||||
mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters
|
||||
}
|
||||
|
||||
try {
|
||||
val kdf = mDatabaseKDBX.getEngineKDBX4(mDatabaseKDBX.kdfParameters)
|
||||
kdf.randomize(mDatabaseKDBX.kdfParameters!!)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.e(TAG, "Unable to retrieve header", unknownKDF)
|
||||
mDatabaseKDBX.randomize()
|
||||
}
|
||||
|
||||
if (header.version.isBefore(FILE_VERSION_40)) {
|
||||
@@ -591,7 +584,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||
|
||||
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
||||
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
|
||||
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
|
||||
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||
}
|
||||
@@ -617,7 +610,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeDeletedObjects(value: List<DeletedObject>) {
|
||||
private fun writeDeletedObjects(value: Collection<DeletedObject>) {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
|
||||
|
||||
for (pdo in value) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database)
|
||||
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database)
|
||||
ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(database)
|
||||
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
|
||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database)
|
||||
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
|
||||
@@ -287,8 +288,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
} finally {
|
||||
// Save the database info before performing action
|
||||
if (intentAction == ACTION_DATABASE_LOAD_TASK) {
|
||||
saveDatabaseInfo()
|
||||
when (intentAction) {
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
saveDatabaseInfo()
|
||||
}
|
||||
}
|
||||
val save = !database.isReadOnly
|
||||
&& (intentAction == ACTION_DATABASE_SAVE
|
||||
@@ -331,6 +336,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
return when (intentAction) {
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK,
|
||||
null -> {
|
||||
START_STICKY
|
||||
@@ -367,6 +373,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
|
||||
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||
else -> {
|
||||
@@ -378,6 +385,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
mMessageId = when (intentAction) {
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> null
|
||||
else -> null
|
||||
}
|
||||
@@ -385,6 +393,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
mWarningId =
|
||||
if (!saveAction
|
||||
|| intentAction == ACTION_DATABASE_LOAD_TASK
|
||||
|| intentAction == ACTION_DATABASE_MERGE_TASK
|
||||
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
|
||||
null
|
||||
else
|
||||
@@ -597,6 +606,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDatabaseMergeActionTask(database: Database): ActionRunnable {
|
||||
return MergeDatabaseRunnable(
|
||||
this,
|
||||
database,
|
||||
this
|
||||
) { result ->
|
||||
// No need to add each info to reload database
|
||||
result.data = Bundle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDatabaseReloadActionTask(database: Database): ActionRunnable {
|
||||
return ReloadDatabaseRunnable(
|
||||
this,
|
||||
@@ -907,6 +927,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
|
||||
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
|
||||
const val ACTION_DATABASE_MERGE_TASK = "ACTION_DATABASE_MERGE_TASK"
|
||||
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
|
||||
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
|
||||
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
|
||||
|
||||
@@ -51,6 +51,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: Database? = null
|
||||
private var mDatabaseReadOnly: Boolean = false
|
||||
private var mMergeDataAllowed: Boolean = false
|
||||
private var mDatabaseAutoSaveEnabled: Boolean = true
|
||||
|
||||
private var mScreen: Screen? = null
|
||||
@@ -115,6 +116,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
mDatabaseViewModel.saveDatabase(save)
|
||||
}
|
||||
|
||||
private fun mergeDatabase() {
|
||||
mDatabaseViewModel.mergeDatabase(false)
|
||||
}
|
||||
|
||||
private fun reloadDatabase() {
|
||||
mDatabaseViewModel.reloadDatabase(false)
|
||||
}
|
||||
@@ -122,6 +127,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
mDatabase = database
|
||||
mDatabaseReadOnly = database?.isReadOnly == true
|
||||
mMergeDataAllowed = database?.isMergeDataAllowed() == true
|
||||
|
||||
mDatabase?.let {
|
||||
if (it.loaded) {
|
||||
@@ -649,6 +655,9 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
if (mDatabaseReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
}
|
||||
if (!mMergeDataAllowed || mDatabaseReadOnly) {
|
||||
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -657,6 +666,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
saveDatabase(!mDatabaseReadOnly)
|
||||
true
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
return true
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
reloadDatabase()
|
||||
return true
|
||||
|
||||
@@ -63,13 +63,11 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
|
||||
super.onDialogClosed(database, positiveResult)
|
||||
if (positiveResult) {
|
||||
database?.let {
|
||||
if (algorithmSelected != null) {
|
||||
val newAlgorithm = algorithmSelected
|
||||
val oldAlgorithm = database.encryptionAlgorithm
|
||||
val newAlgorithm = algorithmSelected
|
||||
val oldAlgorithm = database.encryptionAlgorithm
|
||||
if (newAlgorithm != null) {
|
||||
database.encryptionAlgorithm = newAlgorithm
|
||||
|
||||
if (oldAlgorithm != null && newAlgorithm != null)
|
||||
saveEncryption(oldAlgorithm, newAlgorithm)
|
||||
saveEncryption(oldAlgorithm, newAlgorithm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ class DatabaseViewModel: ViewModel() {
|
||||
val saveDatabase : LiveData<Boolean> get() = _saveDatabase
|
||||
private val _saveDatabase = SingleLiveEvent<Boolean>()
|
||||
|
||||
val mergeDatabase : LiveData<Boolean> get() = _mergeDatabase
|
||||
private val _mergeDatabase = SingleLiveEvent<Boolean>()
|
||||
|
||||
val reloadDatabase : LiveData<Boolean> get() = _reloadDatabase
|
||||
private val _reloadDatabase = SingleLiveEvent<Boolean>()
|
||||
|
||||
@@ -84,6 +87,10 @@ class DatabaseViewModel: ViewModel() {
|
||||
_saveDatabase.value = save
|
||||
}
|
||||
|
||||
fun mergeDatabase(fixDuplicateUuid: Boolean) {
|
||||
_mergeDatabase.value = fixDuplicateUuid
|
||||
}
|
||||
|
||||
fun reloadDatabase(fixDuplicateUuid: Boolean) {
|
||||
_reloadDatabase.value = fixDuplicateUuid
|
||||
}
|
||||
|
||||
10
app/src/main/res/drawable/ic_merge_white_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_merge_white_24dp.xml
Normal 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>
|
||||
@@ -25,10 +25,16 @@
|
||||
android:orderInCategory="95"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item android:id="@+id/menu_reload_database"
|
||||
android:icon="@drawable/ic_downloading_white_24dp"
|
||||
android:title="@string/menu_reload_database"
|
||||
<item android:id="@+id/menu_merge_database"
|
||||
android:icon="@drawable/ic_merge_white_24dp"
|
||||
android:title="@string/menu_merge_database"
|
||||
android:orderInCategory="96"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
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>
|
||||
@@ -226,8 +226,9 @@
|
||||
<string name="menu_cancel">Cancel</string>
|
||||
<string name="menu_hide_password">Hide password</string>
|
||||
<string name="menu_lock">Lock database</string>
|
||||
<string name="menu_save_database">Save database</string>
|
||||
<string name="menu_reload_database">Reload database</string>
|
||||
<string name="menu_save_database">Save data</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_search">Search</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_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_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_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>
|
||||
|
||||
59
art/merge_database.svg
Normal file
59
art/merge_database.svg
Normal 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 |
1
fastlane/metadata/android/en-US/changelogs/93.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/93.txt
Normal file
@@ -0,0 +1 @@
|
||||
* Manage data merge #840 #977
|
||||
1
fastlane/metadata/android/fr-FR/changelogs/93.txt
Normal file
1
fastlane/metadata/android/fr-FR/changelogs/93.txt
Normal file
@@ -0,0 +1 @@
|
||||
* Gestion de la fusion des données #840 #977
|
||||
Reference in New Issue
Block a user