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