mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/Refactor_Kotlin' of github.com:Kunzisoft/KeePassDX into feature/Refactor_Kotlin
This commit is contained in:
@@ -2,6 +2,9 @@ KeepassDX (2.5.0.0beta19)
|
||||
* Add lock button always visible
|
||||
* Refactor connection workflow
|
||||
* Better Magikeyboard connection
|
||||
* Kotlinized code
|
||||
* Fix Recycle Bin
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta18)
|
||||
* New recent databases views
|
||||
|
||||
@@ -63,7 +63,7 @@ Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2017 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePass DX.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -19,18 +19,13 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils;
|
||||
import com.kunzisoft.keepass.utils.UriUtil;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class TestUtil {
|
||||
private static final File sdcard = Environment.getExternalStorageDirectory();
|
||||
|
||||
@@ -68,11 +68,11 @@ public class AESTest extends TestCase {
|
||||
mRand.nextBytes(ivArray);
|
||||
IvParameterSpec iv = new IvParameterSpec(ivArray);
|
||||
|
||||
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true);
|
||||
Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true);
|
||||
android.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
byte[] outAndroid = android.doFinal(input, 0, dataSize);
|
||||
|
||||
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
|
||||
Cipher nat = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding");
|
||||
nat.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ public class CipherTest extends TestCase {
|
||||
rand.nextBytes(iv);
|
||||
rand.nextBytes(plaintext);
|
||||
|
||||
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
|
||||
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
|
||||
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
@@ -78,7 +78,7 @@ public class CipherTest extends TestCase {
|
||||
rand.nextBytes(iv);
|
||||
rand.nextBytes(plaintext);
|
||||
|
||||
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
|
||||
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
|
||||
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
|
||||
@@ -23,16 +23,10 @@ import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwDatabase;
|
||||
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
||||
import com.kunzisoft.keepass.database.element.PwEntryV4;
|
||||
import com.kunzisoft.keepass.utils.SprEngineV4;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
import com.kunzisoft.keepass.database.element.SprEngineV4;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import biz.source_code.base64Coder.Base64Coder;
|
||||
|
||||
public class SprEngineTest extends AndroidTestCase {
|
||||
private PwDatabaseV4 db;
|
||||
@@ -73,14 +67,4 @@ public class SprEngineTest extends AndroidTestCase {
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
private UUID decodeUUID(String encoded) {
|
||||
if (encoded == null || encoded.length() == 0 ) {
|
||||
return PwDatabase.UUID_ZERO;
|
||||
}
|
||||
|
||||
byte[] buf = Base64Coder.decode(encoded);
|
||||
return Types.bytestoUUID(buf);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -142,6 +142,10 @@
|
||||
</activity>
|
||||
|
||||
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.database.action.DatabaseTaskNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Keyboard -->
|
||||
<receiver
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.receiver.NotificationDeleteBroadcastReceiver"
|
||||
|
||||
@@ -42,9 +42,8 @@ class AboutActivity : StylishActivity() {
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = getString(R.string.menu_about)
|
||||
setSupportActionBar(toolbar)
|
||||
assert(supportActionBar != null)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar!!.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
var version: String
|
||||
var build: String
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.notifications.NotificationEntryCopyManager
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
|
||||
@@ -140,7 +141,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
val database = App.currentDatabase
|
||||
database.startManageEntry(entry)
|
||||
// Assign title icon
|
||||
database.drawFactory.assignDatabaseIconTo(this, titleIconView, entry.icon, iconColor)
|
||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
||||
|
||||
// Assign title text
|
||||
titleView?.text = entry.getVisualTitle()
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment.Companion.KEY_ICON_STANDARD
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.view.EntryEditCustomField
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
|
||||
@@ -45,11 +44,15 @@ import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.Util
|
||||
import com.kunzisoft.keepass.utils.applyFontVisibility
|
||||
import com.kunzisoft.keepass.view.EntryEditCustomField
|
||||
|
||||
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
|
||||
|
||||
@@ -140,7 +143,9 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
mEntry = mDatabase?.createEntry()
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon
|
||||
mDatabase?.drawFactory?.assignDefaultDatabaseIconTo(this, entryIconView, iconColor)
|
||||
mDatabase?.drawFactory?.let { iconFactory ->
|
||||
entryIconView?.assignDefaultDatabaseIcon(iconFactory, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the activity if entry or parent can't be retrieve
|
||||
@@ -252,7 +257,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
var actionRunnable: ActionRunnable? = null
|
||||
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
if (actionNodeValues.success)
|
||||
if (actionNodeValues.result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -306,7 +311,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
for (i in 0 until it.childCount) {
|
||||
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
||||
val key = entryEditCustomField.label
|
||||
if (key == null || key.isEmpty()) {
|
||||
if (key.isEmpty()) {
|
||||
validationErrorMessageId = R.string.error_string_key
|
||||
return false
|
||||
}
|
||||
@@ -392,12 +397,8 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
}
|
||||
|
||||
private fun assignIconView() {
|
||||
mEntry?.icon?.let {
|
||||
mDatabase?.drawFactory?.assignDatabaseIconTo(
|
||||
this,
|
||||
entryIconView,
|
||||
it,
|
||||
iconColor)
|
||||
if (mDatabase?.drawFactory != null && mEntry?.icon != null) {
|
||||
entryIconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, mEntry?.icon!!, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,10 +419,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
||||
|
||||
val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this)
|
||||
if (visibilityFontActivated) {
|
||||
Util.applyFontVisibilityTo(this, entryUserNameView)
|
||||
Util.applyFontVisibilityTo(this, entryPasswordView)
|
||||
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView)
|
||||
Util.applyFontVisibilityTo(this, entryCommentView)
|
||||
entryUserNameView?.applyFontVisibility()
|
||||
entryPasswordView?.applyFontVisibility()
|
||||
entryConfirmationPasswordView?.applyFontVisibility()
|
||||
entryCommentView?.applyFontVisibility()
|
||||
}
|
||||
|
||||
if (entry.allowExtraFields()) {
|
||||
|
||||
@@ -49,6 +49,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.*
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
@@ -59,7 +60,6 @@ import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
@@ -170,13 +170,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
|
||||
|
||||
if (fileName!!.isNotEmpty()) {
|
||||
val dbUri = UriUtil.parseDefaultFile(fileName)
|
||||
if (fileName != null && fileName.isNotEmpty()) {
|
||||
val dbUri = UriUtil.parseUriFile(fileName)
|
||||
var scheme: String? = null
|
||||
if (dbUri != null)
|
||||
scheme = dbUri.scheme
|
||||
|
||||
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("file", ignoreCase = true)) {
|
||||
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||
val path = dbUri!!.path
|
||||
val db = File(path!!)
|
||||
|
||||
@@ -404,23 +404,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
|
||||
try {
|
||||
mDatabaseFileUri?.path?.let { databaseFilename ->
|
||||
// Create the new database and start prof
|
||||
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
||||
{
|
||||
CreateDatabaseRunnable(databaseFilename) { database ->
|
||||
// TODO store database created
|
||||
AssignPasswordInDatabaseRunnable(
|
||||
this@FileDatabaseSelectActivity,
|
||||
database,
|
||||
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
|
||||
databaseUri,
|
||||
App.currentDatabase,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile,
|
||||
true, // TODO get readonly
|
||||
LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename))
|
||||
LaunchGroupActivityFinish(databaseUri)
|
||||
)
|
||||
}
|
||||
},
|
||||
R.string.progress_create)
|
||||
.start()
|
||||
@@ -432,7 +430,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
// TODO remove
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
|
||||
@@ -441,9 +438,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
finishRun(true, null)
|
||||
}
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
if (isSuccess) {
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
mFileDatabaseHistory?.addDatabaseUri(fileURI)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
|
||||
@@ -55,10 +55,10 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.action.node.*
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
@@ -193,16 +193,16 @@ class GroupActivity : LockingActivity(),
|
||||
.commit()
|
||||
|
||||
// Add listeners to the add buttons
|
||||
addNodeButtonView?.setAddGroupClickListener {
|
||||
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
||||
GroupEditDialogFragment.build()
|
||||
.show(supportFragmentManager,
|
||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
}
|
||||
addNodeButtonView?.setAddEntryClickListener {
|
||||
})
|
||||
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Search suggestion
|
||||
mDatabase?.let { database ->
|
||||
@@ -348,7 +348,8 @@ class GroupActivity : LockingActivity(),
|
||||
// Assign the group icon depending of IconPack or custom icon
|
||||
iconView?.visibility = View.VISIBLE
|
||||
mCurrentGroup?.let {
|
||||
mDatabase?.drawFactory?.assignDatabaseIconTo(this, iconView, it.icon, mIconColor)
|
||||
if (mDatabase?.drawFactory != null)
|
||||
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor)
|
||||
|
||||
if (toolbar != null) {
|
||||
if (mCurrentGroup?.containsParent() == true)
|
||||
@@ -408,7 +409,7 @@ class GroupActivity : LockingActivity(),
|
||||
EntryActivity.launch(this@GroupActivity, entry, readOnly)
|
||||
},
|
||||
{
|
||||
MagikIME.setEntryKey(getEntry(entry))
|
||||
MagikIME.entryKey = getEntry(entry)
|
||||
// Show the notification if allowed in Preferences
|
||||
if (PreferencesUtil.enableKeyboardNotificationEntry(this@GroupActivity)) {
|
||||
startService(Intent(
|
||||
@@ -442,7 +443,7 @@ class GroupActivity : LockingActivity(),
|
||||
if (entry.containsCustomFields()) {
|
||||
entry.fields
|
||||
.doActionToAllCustomProtectedField { key, value ->
|
||||
entryModel.addCustomField(
|
||||
entryModel.customFields.add(
|
||||
Field(key, value.toString()))
|
||||
}
|
||||
}
|
||||
@@ -603,12 +604,6 @@ class GroupActivity : LockingActivity(),
|
||||
mSearchSuggestionAdapter?.reInit(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// Hide button
|
||||
addNodeButtonView?.hideButton()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
|
||||
val inflater = menuInflater
|
||||
@@ -661,10 +656,10 @@ class GroupActivity : LockingActivity(),
|
||||
// If no node, show education to add new one
|
||||
if (mListNodesFragment != null
|
||||
&& mListNodesFragment!!.isEmpty
|
||||
&& addNodeButtonView != null
|
||||
&& addNodeButtonView?.addButtonView != null
|
||||
&& addNodeButtonView!!.isEnable
|
||||
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
||||
addNodeButtonView!!,
|
||||
addNodeButtonView?.addButtonView!!,
|
||||
{
|
||||
addNodeButtonView?.openButtonIfClose()
|
||||
},
|
||||
@@ -829,7 +824,7 @@ class GroupActivity : LockingActivity(),
|
||||
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.success) {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.addNode(actionNodeValues.newNode)
|
||||
}
|
||||
@@ -840,7 +835,7 @@ class GroupActivity : LockingActivity(),
|
||||
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.success) {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
|
||||
}
|
||||
@@ -851,22 +846,20 @@ class GroupActivity : LockingActivity(),
|
||||
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.success) {
|
||||
if (actionNodeValues.oldNode != null)
|
||||
mListNodesFragment?.removeNode(actionNodeValues.oldNode)
|
||||
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
actionNodeValues.oldNode?.let { oldNode ->
|
||||
oldNode.parent?.let { parent ->
|
||||
|
||||
mListNodesFragment?.removeNode(oldNode)
|
||||
|
||||
// TODO Move trash view
|
||||
// Add trash in views list if it doesn't exists
|
||||
val database = App.currentDatabase
|
||||
if (database.isRecycleBinAvailable && database.isRecycleBinEnabled) {
|
||||
if (database.isRecycleBinEnabled) {
|
||||
val recycleBin = database.recycleBin
|
||||
// Add trash if it doesn't exists
|
||||
if (parent == recycleBin
|
||||
&& mCurrentGroup != null
|
||||
if (mCurrentGroup != null && recycleBin != null
|
||||
&& mCurrentGroup!!.parent == null
|
||||
&& mCurrentGroup != recycleBin) {
|
||||
mListNodesFragment?.addNode(parent)
|
||||
}
|
||||
mListNodesFragment?.addNode(recycleBin)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -877,7 +870,7 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||
name: String?,
|
||||
iconId: PwIcon?) {
|
||||
icon: PwIcon?) {
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
@@ -913,11 +906,15 @@ class GroupActivity : LockingActivity(),
|
||||
true)
|
||||
}
|
||||
// Show the progress dialog now or after dialog confirmation
|
||||
if (database.validatePasswordEncoding(masterPassword!!)) {
|
||||
if (database.validatePasswordEncoding(masterPassword)) {
|
||||
progressDialogThread.start()
|
||||
} else {
|
||||
PasswordEncodingDialogHelper()
|
||||
.show(this, DialogInterface.OnClickListener{ _, _ -> progressDialogThread.start() })
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||
progressDialogThread.start()
|
||||
}
|
||||
show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -940,7 +937,7 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
// Not directly get the entry from intent data but from database
|
||||
// Is refresh from onResume()
|
||||
mListNodesFragment?.rebuildList()
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@@ -976,7 +973,7 @@ class GroupActivity : LockingActivity(),
|
||||
// Else lock if needed
|
||||
else {
|
||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||
App.currentDatabase.closeAndClear(applicationContext)
|
||||
App.currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
moveTaskToBack(true)
|
||||
|
||||
@@ -94,7 +94,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
mAdapter = NodeAdapter(getContextThemed(), currentActivity.menuInflater)
|
||||
mAdapter = NodeAdapter(contextThemed, currentActivity.menuInflater)
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
setIsASearchResult(isASearchResult)
|
||||
@@ -115,7 +115,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(getContextThemed())
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.list_nodes_fragment, container, false)
|
||||
listView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
@@ -129,14 +129,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
})
|
||||
}
|
||||
|
||||
rebuildList()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
rebuildList()
|
||||
|
||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
listView?.visibility = View.GONE
|
||||
@@ -186,6 +186,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
when (item?.itemId) {
|
||||
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment
|
||||
|
||||
/*
|
||||
@@ -206,6 +207,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
//}
|
||||
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogHelper
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.*
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
@@ -59,7 +59,6 @@ import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import permissions.dispatcher.*
|
||||
@@ -265,8 +264,8 @@ class PasswordActivity : StylishActivity(),
|
||||
// Retrieve settings for default database
|
||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
|
||||
if (mDatabaseFileUri != null
|
||||
&& !EmptyUtils.isNullOrEmpty(mDatabaseFileUri!!.path)
|
||||
&& UriUtil.equalsDefaultfile(mDatabaseFileUri, defaultFilename)) {
|
||||
&& mDatabaseFileUri!!.path != null && mDatabaseFileUri!!.path!!.isNotEmpty()
|
||||
&& mDatabaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
|
||||
checkboxDefaultDatabaseView?.isChecked = true
|
||||
}
|
||||
|
||||
@@ -526,9 +525,9 @@ class PasswordActivity : StylishActivity(),
|
||||
setFingerPrintView(R.string.encrypted_value_stored)
|
||||
}
|
||||
|
||||
override fun handleDecryptedResult(passwordValue: String) {
|
||||
override fun handleDecryptedResult(value: String) {
|
||||
// Load database directly
|
||||
verifyKeyFileViewsAndLoadDatabase(passwordValue)
|
||||
verifyKeyFileViewsAndLoadDatabase(value)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
@@ -563,8 +562,8 @@ class PasswordActivity : StylishActivity(),
|
||||
|
||||
private fun verifyAllViewsAndLoadDatabase() {
|
||||
verifyCheckboxesAndLoadDatabase(
|
||||
passwordView?.text.toString(),
|
||||
UriUtil.parseDefaultFile(keyFileView?.text.toString()))
|
||||
passwordView?.text?.toString(),
|
||||
UriUtil.parseUriFile(keyFileView?.text?.toString()))
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(password: String?, keyFile: Uri?) {
|
||||
@@ -580,8 +579,8 @@ class PasswordActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
private fun verifyKeyFileViewsAndLoadDatabase(password: String) {
|
||||
val key = keyFileView?.text.toString()
|
||||
var keyUri = UriUtil.parseDefaultFile(key)
|
||||
val key = keyFileView?.text?.toString()
|
||||
var keyUri = UriUtil.parseUriFile(key)
|
||||
if (checkboxKeyFileView?.isChecked != true) {
|
||||
keyUri = null
|
||||
}
|
||||
@@ -591,20 +590,20 @@ class PasswordActivity : StylishActivity(),
|
||||
private fun loadDatabase(password: String?, keyFile: Uri?) {
|
||||
// Clear before we load
|
||||
val database = App.currentDatabase
|
||||
database.closeAndClear(applicationContext)
|
||||
database.closeAndClear(applicationContext.filesDir)
|
||||
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
ProgressDialogThread(this,
|
||||
{ progressTaskUpdater ->
|
||||
LoadDatabaseRunnable(
|
||||
WeakReference(this@PasswordActivity.applicationContext),
|
||||
WeakReference(this@PasswordActivity),
|
||||
database,
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
progressTaskUpdater,
|
||||
AfterLoadingDatabase(database))
|
||||
AfterLoadingDatabase(database, password))
|
||||
},
|
||||
R.string.loading_database).start()
|
||||
}
|
||||
@@ -613,9 +612,11 @@ class PasswordActivity : StylishActivity(),
|
||||
/**
|
||||
* Called after verify and try to opening the database
|
||||
*/
|
||||
private inner class AfterLoadingDatabase internal constructor(var database: Database) : ActionRunnable() {
|
||||
private inner class AfterLoadingDatabase internal constructor(var database: Database,
|
||||
val password: String?)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
// Recheck fingerprint if error
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
@@ -623,15 +624,20 @@ class PasswordActivity : StylishActivity(),
|
||||
reInitWithFingerprintMode()
|
||||
}
|
||||
|
||||
if (database.isPasswordEncodingError) {
|
||||
val dialog = PasswordEncodingDialogHelper()
|
||||
dialog.show(this@PasswordActivity,
|
||||
DialogInterface.OnClickListener { _, _ -> launchGroupActivity() })
|
||||
} else if (isSuccess) {
|
||||
if (result.isSuccess) {
|
||||
if (database.validatePasswordEncoding(password)) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
if (message != null && message.isNotEmpty()) {
|
||||
Toast.makeText(this@PasswordActivity, message, Toast.LENGTH_LONG).show()
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||
launchGroupActivity()
|
||||
}
|
||||
show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (result.message != null && result.message!!.isNotEmpty()) {
|
||||
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -762,7 +768,7 @@ class PasswordActivity : StylishActivity(),
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
||||
setEmptyViews()
|
||||
App.currentDatabase.closeAndClear(applicationContext)
|
||||
App.currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,14 +820,13 @@ class PasswordActivity : StylishActivity(),
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
private fun verifyFileNameUriFromLaunch(fileName: String) {
|
||||
if (EmptyUtils.isNullOrEmpty(fileName)) {
|
||||
if (fileName.isEmpty()) {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
|
||||
val uri = UriUtil.parseDefaultFile(fileName)
|
||||
val scheme = uri.scheme
|
||||
|
||||
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equals("file", ignoreCase = true)) {
|
||||
val uri = UriUtil.parseUriFile(fileName)
|
||||
val scheme = uri?.scheme
|
||||
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||
val dbFile = File(uri.path!!)
|
||||
if (!dbFile.exists()) {
|
||||
throw FileNotFoundException()
|
||||
|
||||
@@ -35,7 +35,6 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
@@ -178,13 +177,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
private fun verifyFile(): Boolean {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked
|
||||
&& keyFileView != null) {
|
||||
val keyFile = UriUtil.parseDefaultFile(keyFileView!!.text.toString())
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
|
||||
mKeyFile = keyFile
|
||||
|
||||
// Verify that a keyfile is set
|
||||
if (EmptyUtils.isNullOrEmpty(keyFile)) {
|
||||
if (keyFile == null || keyFile.toString().isEmpty()) {
|
||||
error = true
|
||||
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -229,11 +227,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
uri?.let { currentUri ->
|
||||
UriUtil.parseDefaultFile(currentUri.toString())?.let { pathString ->
|
||||
UriUtil.parseUriFile(uri)?.let { pathUri ->
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileView?.text = pathString.toString()
|
||||
}
|
||||
keyFileView?.text = pathUri.toString()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ class BrowserDialogFragment : DialogFragment() {
|
||||
|
||||
val market = root.findViewById<Button>(R.id.install_market)
|
||||
market.setOnClickListener {
|
||||
Util.gotoUrl(context, R.string.filemanager_play_store)
|
||||
Util.gotoUrl(context!!, R.string.filemanager_play_store)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
val web = root.findViewById<Button>(R.id.install_web)
|
||||
web.setOnClickListener {
|
||||
Util.gotoUrl(context, R.string.filemanager_f_droid)
|
||||
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
|
||||
|
||||
// Spinner Drop down elements
|
||||
val fileTypes = resources.getStringArray(R.array.file_types)
|
||||
val dataAdapter = ArrayAdapter(activity!!, android.R.layout.simple_spinner_item, fileTypes)
|
||||
val dataAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, fileTypes)
|
||||
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = dataAdapter
|
||||
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
|
||||
@@ -158,7 +158,7 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
dialog.setOnShowListener { _ ->
|
||||
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
positiveButton?.setOnClickListener { _ ->
|
||||
@@ -181,11 +181,13 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
|
||||
}
|
||||
|
||||
private fun buildPath(): Uri? {
|
||||
if (folderPathView != null && mDatabaseFileExtension != null) {
|
||||
if (folderPathView != null && fileNameView != null && mDatabaseFileExtension != null) {
|
||||
var path = Uri.Builder().path(folderPathView!!.text.toString())
|
||||
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
|
||||
.build()
|
||||
path = UriUtil.translate(context, path)
|
||||
context?.let { context ->
|
||||
path = UriUtil.translateUri(context, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.Util
|
||||
import com.kunzisoft.keepass.utils.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -66,7 +67,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
root = inflater.inflate(R.layout.generate_password, null)
|
||||
|
||||
passwordView = root?.findViewById(R.id.password)
|
||||
Util.applyFontVisibilityTo(context, passwordView)
|
||||
passwordView?.applyFontVisibility()
|
||||
|
||||
lengthTextView = root?.findViewById(R.id.length)
|
||||
|
||||
@@ -92,7 +93,10 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
|
||||
context?.let { context ->
|
||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
||||
}
|
||||
|
||||
root?.findViewById<Button>(R.id.generate_password_button)
|
||||
?.setOnClickListener { fillPassword() }
|
||||
@@ -131,8 +135,9 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
bracketsBox?.isChecked = false
|
||||
extendedBox?.isChecked = false
|
||||
|
||||
val defaultPasswordChars = PreferencesUtil.getDefaultPasswordCharacters(context)
|
||||
for (passwordChar in defaultPasswordChars) {
|
||||
context?.let { context ->
|
||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
||||
for (passwordChar in charSet) {
|
||||
when (passwordChar) {
|
||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
||||
@@ -146,6 +151,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillPassword() {
|
||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
||||
@@ -156,7 +163,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
try {
|
||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||
|
||||
val generator = PasswordGenerator(activity)
|
||||
val generator = PasswordGenerator(resources)
|
||||
password = generator.generatePassword(length,
|
||||
uppercaseBox?.isChecked == true,
|
||||
lowercaseBox?.isChecked == true,
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwIcon
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
|
||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
||||
|
||||
@@ -45,7 +46,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
private var nameGroup: String? = null
|
||||
private var iconGroup: PwIcon? = null
|
||||
|
||||
private var iconButton: ImageView? = null
|
||||
private var iconButtonView: ImageView? = null
|
||||
private var iconColor: Int = 0
|
||||
|
||||
enum class EditGroupDialogAction {
|
||||
@@ -76,7 +77,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.group_edit, null)
|
||||
val nameField = root?.findViewById<TextView>(R.id.group_edit_name)
|
||||
iconButton = root?.findViewById(R.id.group_edit_icon_button)
|
||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
@@ -133,7 +134,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
this@GroupEditDialogFragment.dialog.cancel()
|
||||
}
|
||||
|
||||
iconButton?.setOnClickListener { _ ->
|
||||
iconButtonView?.setOnClickListener { _ ->
|
||||
fragmentManager?.let {
|
||||
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
||||
}
|
||||
@@ -145,12 +146,9 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
}
|
||||
|
||||
private fun assignIconView() {
|
||||
mDatabase?.drawFactory
|
||||
?.assignDatabaseIconTo(
|
||||
context,
|
||||
iconButton,
|
||||
iconGroup,
|
||||
iconColor)
|
||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
|
||||
@@ -61,7 +61,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
iconPack = IconPackChooser.getSelectedIconPack(context)
|
||||
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
||||
|
||||
// Inflate and set the layout for the dialog
|
||||
// Pass null as the parent view because its going in the dialog layout
|
||||
|
||||
@@ -47,9 +47,9 @@ class KeyboardExplanationDialogFragment : DialogFragment() {
|
||||
|
||||
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context, R.string.keyboard_switcher_play_store) }
|
||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||
} else {
|
||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context, R.string.keyboard_switcher_f_droid) }
|
||||
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
|
||||
}
|
||||
|
||||
builder.setView(rootView)
|
||||
|
||||
@@ -20,25 +20,27 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class PasswordEncodingDialogHelper {
|
||||
private var dialog: AlertDialog? = null
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
@JvmOverloads
|
||||
fun show(ctx: Context, onclick: DialogInterface.OnClickListener, showCancel: Boolean = false) {
|
||||
val builder = AlertDialog.Builder(ctx)
|
||||
builder.setMessage(R.string.warning_password_encoding).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, onclick)
|
||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
|
||||
if (showCancel) {
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
dialog = builder.create()
|
||||
dialog?.show()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class ProFeatureDialogFragment : DialogFragment() {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context, R.string.app_pro_url)
|
||||
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class ProFeatureDialogFragment : DialogFragment() {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context, R.string.contribution_url)
|
||||
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity!!)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
@@ -49,32 +49,32 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
|
||||
builder.setPositiveButton(android.R.string.ok) { dialog, id -> dismiss() }
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||
} else {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||
builder.setPositiveButton(R.string.download) { dialog, id ->
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context, R.string.app_pro_url)
|
||||
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, id -> dismiss() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
|
||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||
builder.setPositiveButton(R.string.contribute) { dialog, id ->
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
try {
|
||||
Util.gotoUrl(context, R.string.contribution_url)
|
||||
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, id -> dismiss() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
// Create the AlertDialog object and return it
|
||||
|
||||
@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.app.Fragment
|
||||
@@ -30,7 +32,6 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.fileselect.StorageAF
|
||||
import com.kunzisoft.keepass.utils.Interaction
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class KeyFileHelper {
|
||||
@@ -55,7 +56,7 @@ class KeyFileHelper {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
try {
|
||||
if (StorageAF.useStorageFramework(activity)) {
|
||||
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
|
||||
openActivityWithActionOpenDocument()
|
||||
} else {
|
||||
openActivityWithActionGetContent()
|
||||
@@ -67,7 +68,6 @@ class KeyFileHelper {
|
||||
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
||||
showBrowserDialog()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class KeyFileHelper {
|
||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
||||
var showBrowser = false
|
||||
try {
|
||||
if (Interaction.isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||
// Get file path parent if possible
|
||||
if (dataUri != null
|
||||
@@ -130,6 +130,26 @@ class KeyFileHelper {
|
||||
return showBrowser
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the specified action can be used as an intent. This
|
||||
* method queries the package manager for installed packages that can
|
||||
* respond to an intent with the specified action. If no suitable package is
|
||||
* found, this method returns false.
|
||||
*
|
||||
* @param context The application's environment.
|
||||
* @param action The Intent action to check for availability.
|
||||
*
|
||||
* @return True if an Intent with the specified action can be sent and
|
||||
* responded to, false otherwise.
|
||||
*/
|
||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
||||
val packageManager = context.packageManager
|
||||
val intent = Intent(action)
|
||||
val list = packageManager.queryIntentActivities(intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY)
|
||||
return list.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Browser dialog to select file picker app
|
||||
*/
|
||||
@@ -162,7 +182,7 @@ class KeyFileHelper {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parseDefaultFile(filename)
|
||||
keyUri = UriUtil.parseUriFile(filename)
|
||||
}
|
||||
keyFileCallback?.invoke(keyUri)
|
||||
}
|
||||
@@ -173,7 +193,7 @@ class KeyFileHelper {
|
||||
if (data != null) {
|
||||
var uri = data.data
|
||||
if (uri != null) {
|
||||
if (StorageAF.useStorageFramework(activity)) {
|
||||
if (StorageAF.useStorageFramework(activity!!)) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
@@ -185,10 +205,9 @@ class KeyFileHelper {
|
||||
} catch (e: Exception) {
|
||||
// nop
|
||||
}
|
||||
|
||||
}
|
||||
if (requestCode == GET_CONTENT) {
|
||||
uri = UriUtil.translate(activity, uri)
|
||||
uri = UriUtil.translateUri(activity!!, uri)
|
||||
}
|
||||
keyFileCallback?.invoke(uri)
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ class UriIntentInitTask(private val weakContext: WeakReference<Context>,
|
||||
}
|
||||
|
||||
} else {
|
||||
databaseUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_FILENAME))
|
||||
keyFileUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_KEYFILE))
|
||||
databaseUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_FILENAME))
|
||||
keyFileUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_KEYFILE))
|
||||
|
||||
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
|
||||
keyFileUri = getKeyFileUri(databaseUri)
|
||||
|
||||
@@ -71,13 +71,12 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
if (timeoutEnable) {
|
||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this)) {
|
||||
lockReceiver = LockReceiver()
|
||||
val intentFilter = IntentFilter()
|
||||
intentFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
intentFilter.addAction(LOCK_ACTION)
|
||||
registerReceiver(lockReceiver, IntentFilter(intentFilter))
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(LOCK_ACTION)
|
||||
}
|
||||
registerReceiver(lockReceiver, IntentFilter(intentFilter))
|
||||
}
|
||||
|
||||
exitLock = false
|
||||
@@ -190,7 +189,7 @@ fun Activity.lock() {
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||
cancelAll()
|
||||
}
|
||||
App.currentDatabase.closeAndClear(applicationContext)
|
||||
App.currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StyleRes;
|
||||
import android.util.Log;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
||||
|
||||
/**
|
||||
* FilePickerActivity class with a style compatibility
|
||||
*/
|
||||
public class FilePickerStylishActivity extends FilePickerActivity {
|
||||
|
||||
private @StyleRes
|
||||
int themeId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
this.themeId = FilePickerStylish.getThemeId(this);
|
||||
setTheme(themeId);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if(FilePickerStylish.getThemeId(this) != this.themeId) {
|
||||
Log.d(this.getClass().getName(), "Theme change detected, restarting activity");
|
||||
this.recreate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived from the Stylish class, get the specific FilePickerStyle theme
|
||||
*/
|
||||
public static class FilePickerStylish extends Stylish {
|
||||
public static @StyleRes int getThemeId(Context context) {
|
||||
if (themeString.equals(context.getString(R.string.list_style_name_dark)))
|
||||
return R.style.KeepassDXStyle_FilePickerStyle_Dark;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_blue)))
|
||||
return R.style.KeepassDXStyle_FilePickerStyle_Blue;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_red)))
|
||||
return R.style.KeepassDXStyle_FilePickerStyle_Red;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_purple)))
|
||||
return R.style.KeepassDXStyle_FilePickerStyle_Purple;
|
||||
|
||||
return R.style.KeepassDXStyle_FilePickerStyle;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.StyleRes
|
||||
import android.util.Log
|
||||
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||
|
||||
/**
|
||||
* FilePickerActivity class with a style compatibility
|
||||
*/
|
||||
class FilePickerStylishActivity : FilePickerActivity() {
|
||||
|
||||
@StyleRes
|
||||
private var themeId: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
this.themeId = Stylish.getFilePickerThemeId(this)
|
||||
setTheme(themeId)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (Stylish.getFilePickerThemeId(this) != this.themeId) {
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
this.recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.StyleRes;
|
||||
import android.support.v7.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
|
||||
/**
|
||||
* Class that provides functions to retrieve and assign a theme to a module
|
||||
*/
|
||||
public class Stylish {
|
||||
|
||||
protected static String themeString;
|
||||
|
||||
/**
|
||||
* Initialize the class with a theme preference
|
||||
* @param context Context to retrieve the theme preference
|
||||
*/
|
||||
public static void init(Context context) {
|
||||
String stylishPrefKey = context.getString(R.string.setting_style_key);
|
||||
Log.d(Stylish.class.getName(), "Attatching to " + context.getPackageName());
|
||||
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the style to the class attribute
|
||||
* @param styleString Style id String
|
||||
*/
|
||||
public static void assignStyle(String styleString) {
|
||||
themeString = styleString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that returns the current id of the style selected in the preference
|
||||
* @param context Context to retrieve the id
|
||||
* @return Id of the style
|
||||
*/
|
||||
public static @StyleRes int getThemeId(Context context) {
|
||||
|
||||
if (themeString.equals(context.getString(R.string.list_style_name_night)))
|
||||
return R.style.KeepassDXStyle_Night;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_dark)))
|
||||
return R.style.KeepassDXStyle_Dark;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_blue)))
|
||||
return R.style.KeepassDXStyle_Blue;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_red)))
|
||||
return R.style.KeepassDXStyle_Red;
|
||||
else if (themeString.equals(context.getString(R.string.list_style_name_purple)))
|
||||
return R.style.KeepassDXStyle_Purple;
|
||||
|
||||
return R.style.KeepassDXStyle_Light;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.StyleRes
|
||||
import android.support.v7.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
/**
|
||||
* Class that provides functions to retrieve and assign a theme to a module
|
||||
*/
|
||||
object Stylish {
|
||||
|
||||
private var themeString: String? = null
|
||||
|
||||
/**
|
||||
* Initialize the class with a theme preference
|
||||
* @param context Context to retrieve the theme preference
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light))
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the style to the class attribute
|
||||
* @param styleString Style id String
|
||||
*/
|
||||
fun assignStyle(styleString: String) {
|
||||
themeString = styleString
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that returns the current id of the style selected in the preference
|
||||
* @param context Context to retrieve the id
|
||||
* @return Id of the style
|
||||
*/
|
||||
@StyleRes
|
||||
fun getThemeId(context: Context): Int {
|
||||
|
||||
return when (themeString) {
|
||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
||||
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
||||
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||
else -> R.style.KeepassDXStyle_Light
|
||||
}
|
||||
}
|
||||
|
||||
@StyleRes
|
||||
fun getFilePickerThemeId(context: Context): Int {
|
||||
return when {
|
||||
themeString.equals(context.getString(R.string.list_style_name_dark)) -> R.style.KeepassDXStyle_FilePickerStyle_Dark
|
||||
themeString.equals(context.getString(R.string.list_style_name_blue)) -> R.style.KeepassDXStyle_FilePickerStyle_Blue
|
||||
themeString.equals(context.getString(R.string.list_style_name_red)) -> R.style.KeepassDXStyle_FilePickerStyle_Red
|
||||
themeString.equals(context.getString(R.string.list_style_name_purple)) -> R.style.KeepassDXStyle_FilePickerStyle_Purple
|
||||
else -> R.style.KeepassDXStyle_FilePickerStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StyleRes;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
|
||||
public abstract class StylishActivity extends AppCompatActivity {
|
||||
|
||||
private @StyleRes int themeId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.themeId = Stylish.getThemeId(this);
|
||||
setTheme(themeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if(Stylish.getThemeId(this) != this.themeId) {
|
||||
Log.d(this.getClass().getName(), "Theme change detected, restarting activity");
|
||||
this.recreate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.StyleRes
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
|
||||
abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
@StyleRes
|
||||
private var themeId: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (Stylish.getThemeId(this) != this.themeId) {
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
this.recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StyleRes;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
|
||||
public abstract class StylishFragment extends Fragment {
|
||||
|
||||
protected @StyleRes int themeId;
|
||||
protected Context contextThemed; // TODO small ref
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context != null) {
|
||||
this.themeId = Stylish.getThemeId(context);
|
||||
}
|
||||
contextThemed = new ContextThemeWrapper(context, themeId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
// To fix status bar color
|
||||
if (getActivity() != null
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
|
||||
int[] attrColorPrimaryDark = {android.R.attr.colorPrimaryDark};
|
||||
TypedArray taColorPrimaryDark = contextThemed.getTheme().obtainStyledAttributes(attrColorPrimaryDark);
|
||||
window.setStatusBarColor(taColorPrimaryDark.getColor(0, Color.BLACK));
|
||||
taColorPrimaryDark.recycle();
|
||||
}
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState);
|
||||
}
|
||||
|
||||
public Context getContextThemed() {
|
||||
return contextThemed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.StyleRes
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
abstract class StylishFragment : Fragment() {
|
||||
|
||||
@StyleRes
|
||||
protected var themeId: Int = 0
|
||||
protected lateinit var contextThemed: Context
|
||||
|
||||
override fun onAttach(context: Context?) {
|
||||
super.onAttach(context)
|
||||
if (context != null) {
|
||||
this.themeId = Stylish.getThemeId(context)
|
||||
}
|
||||
contextThemed = ContextThemeWrapper(context, themeId)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// To fix status bar color
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity!!.window
|
||||
|
||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
||||
val taColorPrimaryDark = contextThemed.theme.obtainStyledAttributes(attrColorPrimaryDark)
|
||||
window.statusBarColor = taColorPrimaryDark.getColor(0, Color.BLACK)
|
||||
taColorPrimaryDark.recycle()
|
||||
}
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.Util
|
||||
|
||||
@@ -118,7 +119,7 @@ class NodeAdapter
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
val textSizeDefault = Util.getListTextDefaultSize(context)
|
||||
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
|
||||
this.textSize = PreferencesUtil.getListTextSize(context)
|
||||
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
|
||||
// Retrieve the icon size
|
||||
@@ -207,7 +208,7 @@ class NodeAdapter
|
||||
Type.GROUP -> iconGroupColor
|
||||
Type.ENTRY -> iconEntryColor
|
||||
}
|
||||
mDatabase.drawFactory.assignDatabaseIconTo(context, holder.icon, subNode.icon, iconColor)
|
||||
holder.icon?.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
// Assign text
|
||||
holder.text?.text = subNode.title
|
||||
// Assign click
|
||||
|
||||
@@ -33,10 +33,12 @@ import com.kunzisoft.keepass.database.cursor.EntryCursor
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwIcon
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class SearchEntryCursorAdapter(context: Context, private val database: Database) : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
@@ -89,7 +91,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
|
||||
// Assign image
|
||||
database.drawFactory.assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor)
|
||||
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
|
||||
|
||||
// Assign title
|
||||
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
|
||||
@@ -108,7 +110,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
}
|
||||
|
||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||
return database.searchEntry(constraint.toString())
|
||||
return database.searchEntries(constraint.toString())
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): EntryVersioned? {
|
||||
|
||||
@@ -37,7 +37,7 @@ class App : MultiDexApplication() {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
currentDatabase.closeAndClear(applicationContext)
|
||||
currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
@@ -17,20 +17,18 @@
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto;
|
||||
package com.kunzisoft.keepass.crypto
|
||||
|
||||
import java.security.Provider;
|
||||
import java.security.Provider
|
||||
|
||||
public final class AESProvider extends Provider {
|
||||
class AESProvider : Provider("AESProvider", 1.0, "") {
|
||||
init {
|
||||
put("Cipher.AES", NativeAESCipherSpi::class.java.name)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -3846349284296062658L;
|
||||
companion object {
|
||||
|
||||
public AESProvider() {
|
||||
super("AESProvider", 1.0, "");
|
||||
put("Cipher.AES",NativeAESCipherSpi.class.getName());
|
||||
private const val serialVersionUID = -3846349284296062658L
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine;
|
||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine;
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine;
|
||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine;
|
||||
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Security;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
public class CipherFactory {
|
||||
private static boolean blacklistInit = false;
|
||||
private static boolean blacklisted;
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public static Cipher getInstance(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
return getInstance(transformation, false);
|
||||
}
|
||||
|
||||
public static Cipher getInstance(String transformation, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Return the native AES if it is possible
|
||||
if ( (!deviceBlacklisted()) && (!androidOverride) && hasNativeImplementation(transformation) && NativeLib.loaded() ) {
|
||||
return Cipher.getInstance(transformation, new AESProvider());
|
||||
} else {
|
||||
return Cipher.getInstance(transformation);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean deviceBlacklisted() {
|
||||
if (!blacklistInit) {
|
||||
blacklistInit = true;
|
||||
|
||||
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
|
||||
blacklisted = Build.MODEL.equals("A500");
|
||||
}
|
||||
return blacklisted;
|
||||
}
|
||||
|
||||
private static boolean hasNativeImplementation(String transformation) {
|
||||
return transformation.equals("AES/CBC/PKCS5Padding");
|
||||
}
|
||||
|
||||
|
||||
/** Generate appropriate cipher based on KeePass 2.x UUID's
|
||||
* @param uuid
|
||||
* @return
|
||||
* @throws NoSuchPaddingException
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws InvalidAlgorithmParameterException
|
||||
* @throws InvalidKeyException
|
||||
*/
|
||||
public static CipherEngine getInstance(UUID uuid) throws NoSuchAlgorithmException {
|
||||
if ( uuid.equals(AesEngine.CIPHER_UUID) ) {
|
||||
return new AesEngine();
|
||||
} else if ( uuid.equals(TwofishEngine.CIPHER_UUID) ) {
|
||||
return new TwofishEngine();
|
||||
} else if ( uuid.equals(ChaCha20Engine.CIPHER_UUID)) {
|
||||
return new ChaCha20Engine();
|
||||
}
|
||||
|
||||
throw new NoSuchAlgorithmException("UUID unrecognized.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto
|
||||
|
||||
import android.os.Build
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Security
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
|
||||
object CipherFactory {
|
||||
|
||||
private var blacklistInit = false
|
||||
private var blacklisted: Boolean = false
|
||||
|
||||
init {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
||||
@JvmOverloads
|
||||
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
||||
// Return the native AES if it is possible
|
||||
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
||||
Cipher.getInstance(transformation, AESProvider())
|
||||
} else {
|
||||
Cipher.getInstance(transformation)
|
||||
}
|
||||
}
|
||||
|
||||
fun deviceBlacklisted(): Boolean {
|
||||
if (!blacklistInit) {
|
||||
blacklistInit = true
|
||||
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
|
||||
blacklisted = Build.MODEL == "A500"
|
||||
}
|
||||
return blacklisted
|
||||
}
|
||||
|
||||
private fun hasNativeImplementation(transformation: String): Boolean {
|
||||
return transformation == "AES/CBC/PKCS5Padding"
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate appropriate cipher based on KeePass 2.x UUID's
|
||||
*/
|
||||
@Throws(NoSuchAlgorithmException::class)
|
||||
fun getInstance(uuid: UUID): CipherEngine {
|
||||
return when (uuid) {
|
||||
AesEngine.CIPHER_UUID -> AesEngine()
|
||||
TwofishEngine.CIPHER_UUID -> TwofishEngine()
|
||||
ChaCha20Engine.CIPHER_UUID -> ChaCha20Engine()
|
||||
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto;
|
||||
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream;
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
public class CryptoUtil {
|
||||
public static byte[] resizeKey(byte[] in, int inOffset, int cbIn, int cbOut) {
|
||||
if (cbOut == 0) return new byte[0];
|
||||
|
||||
byte[] hash;
|
||||
if (cbOut <= 32) { hash = hashSha256(in, inOffset, cbIn); }
|
||||
else { hash = hashSha512(in, inOffset, cbIn); }
|
||||
|
||||
if (cbOut == hash.length) { return hash; }
|
||||
|
||||
byte[] ret = new byte[cbOut];
|
||||
if (cbOut < hash.length) {
|
||||
System.arraycopy(hash, 0, ret, 0, cbOut);
|
||||
}
|
||||
else {
|
||||
int pos = 0;
|
||||
long r = 0;
|
||||
while (pos < cbOut) {
|
||||
Mac hmac;
|
||||
try {
|
||||
hmac = Mac.getInstance("HmacSHA256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
byte[] pbR = LEDataOutputStream.writeLongBuf(r);
|
||||
byte[] part = hmac.doFinal(pbR);
|
||||
|
||||
int copy = Math.min(cbOut - pos, part.length);
|
||||
assert(copy > 0);
|
||||
|
||||
System.arraycopy(part, 0, ret, pos, copy);
|
||||
pos += copy;
|
||||
r++;
|
||||
|
||||
Arrays.fill(part, (byte)0);
|
||||
}
|
||||
assert(pos == cbOut);
|
||||
}
|
||||
|
||||
Arrays.fill(hash, (byte)0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] hashSha256(byte[] data) {
|
||||
return hashSha256(data, 0, data.length);
|
||||
}
|
||||
|
||||
public static byte[] hashSha256(byte[] data, int offset, int count) {
|
||||
return hashGen("SHA-256", data, offset, count);
|
||||
}
|
||||
|
||||
public static byte[] hashSha512(byte[] data) {
|
||||
return hashSha512(data, 0, data.length);
|
||||
}
|
||||
|
||||
public static byte[] hashSha512(byte[] data, int offset, int count) {
|
||||
return hashGen("SHA-512", data, offset, count);
|
||||
}
|
||||
|
||||
public static byte[] hashGen(String transform, byte[] data, int offset, int count) {
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance(transform);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
NullOutputStream nos = new NullOutputStream();
|
||||
DigestOutputStream dos = new DigestOutputStream(nos, hash);
|
||||
|
||||
try {
|
||||
dos.write(data, offset, count);
|
||||
dos.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return hash.digest();
|
||||
}
|
||||
}
|
||||
109
app/src/main/java/com/kunzisoft/keepass/crypto/CryptoUtil.kt
Normal file
109
app/src/main/java/com/kunzisoft/keepass/crypto/CryptoUtil.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto
|
||||
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
|
||||
import java.io.IOException
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Arrays
|
||||
|
||||
import javax.crypto.Mac
|
||||
import kotlin.math.min
|
||||
|
||||
object CryptoUtil {
|
||||
|
||||
fun resizeKey(inBytes: ByteArray, inOffset: Int, cbIn: Int, cbOut: Int): ByteArray {
|
||||
if (cbOut == 0) return ByteArray(0)
|
||||
|
||||
val hash: ByteArray = if (cbOut <= 32) {
|
||||
hashSha256(inBytes, inOffset, cbIn)
|
||||
} else {
|
||||
hashSha512(inBytes, inOffset, cbIn)
|
||||
}
|
||||
|
||||
if (cbOut == hash.size) {
|
||||
return hash
|
||||
}
|
||||
|
||||
val ret = ByteArray(cbOut)
|
||||
if (cbOut < hash.size) {
|
||||
System.arraycopy(hash, 0, ret, 0, cbOut)
|
||||
} else {
|
||||
var pos = 0
|
||||
var r: Long = 0
|
||||
while (pos < cbOut) {
|
||||
val hmac: Mac
|
||||
try {
|
||||
hmac = Mac.getInstance("HmacSHA256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
val pbR = LEDataOutputStream.writeLongBuf(r)
|
||||
val part = hmac.doFinal(pbR)
|
||||
|
||||
val copy = min(cbOut - pos, part.size)
|
||||
System.arraycopy(part, 0, ret, pos, copy)
|
||||
pos += copy
|
||||
r++
|
||||
|
||||
Arrays.fill(part, 0.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
Arrays.fill(hash, 0.toByte())
|
||||
return ret
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||
return hashGen("SHA-256", data, offset, count)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||
return hashGen("SHA-512", data, offset, count)
|
||||
}
|
||||
|
||||
private fun hashGen(transform: String, data: ByteArray, offset: Int, count: Int): ByteArray {
|
||||
val hash: MessageDigest
|
||||
try {
|
||||
hash = MessageDigest.getInstance(transform)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
val nos = NullOutputStream()
|
||||
val dos = DigestOutputStream(nos, hash)
|
||||
|
||||
try {
|
||||
dos.write(data, offset, count)
|
||||
dos.close()
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
return hash.digest()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
@@ -47,14 +47,13 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
private static final String TAG = NativeAESCipherSpi.class.getName();
|
||||
|
||||
private static boolean mIsStaticInit = false;
|
||||
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<PhantomReference<NativeAESCipherSpi>, Long>();
|
||||
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<NativeAESCipherSpi>();
|
||||
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<>();
|
||||
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<>();
|
||||
|
||||
private final int AES_BLOCK_SIZE = 16;
|
||||
private byte[] mIV;
|
||||
|
||||
private boolean mIsInited = false;
|
||||
private boolean mEncrypting = false;
|
||||
private boolean mIsInit = false;
|
||||
private long mCtxPtr;
|
||||
|
||||
private boolean mPadding = false;
|
||||
@@ -68,13 +67,12 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
|
||||
private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) {
|
||||
Log.d(TAG, "queued cipher context: " + ptr);
|
||||
mCleanup.put(new PhantomReference<NativeAESCipherSpi>(ref, mQueue), ptr);
|
||||
mCleanup.put(new PhantomReference<>(ref, mQueue), ptr);
|
||||
}
|
||||
|
||||
/** Work with the garbage collector to clean up openssl memory when the cipher
|
||||
* context is garbage collected.
|
||||
* @author bpellin
|
||||
*
|
||||
*/
|
||||
private static class Cleanup implements Runnable {
|
||||
|
||||
@@ -92,13 +90,12 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static native void nCleanup(long ctxPtr);
|
||||
|
||||
public NativeAESCipherSpi() {
|
||||
if ( ! mIsStaticInit ) {
|
||||
if ( !mIsStaticInit ) {
|
||||
staticInit();
|
||||
}
|
||||
}
|
||||
@@ -134,11 +131,9 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
IllegalBlockSizeException, BadPaddingException {
|
||||
|
||||
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
|
||||
|
||||
if ( result == -1 ) {
|
||||
throw new ShortBufferException();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -146,7 +141,6 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
|
||||
|
||||
int outputSize = engineGetOutputSize(inputLen);
|
||||
|
||||
int updateAmt;
|
||||
if (input != null && inputLen > 0) {
|
||||
updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
|
||||
@@ -155,11 +149,7 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
}
|
||||
|
||||
int finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt);
|
||||
|
||||
int out = updateAmt + finalAmt;
|
||||
|
||||
|
||||
return out;
|
||||
return updateAmt + finalAmt;
|
||||
}
|
||||
|
||||
private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize)
|
||||
@@ -231,22 +221,19 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
} catch (InvalidParameterSpecException e) {
|
||||
throw new InvalidAlgorithmParameterException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void init(int opmode, Key key, IvParameterSpec params) {
|
||||
if ( mIsInited ) {
|
||||
if (mIsInit) {
|
||||
// Do not allow multiple inits
|
||||
assert(true);
|
||||
throw new RuntimeException("Don't allow multiple inits");
|
||||
} else {
|
||||
NativeLib.init();
|
||||
mIsInited = true;
|
||||
NativeLib.INSTANCE.init();
|
||||
mIsInit = true;
|
||||
}
|
||||
|
||||
mIV = params.getIV();
|
||||
mEncrypting = opmode == Cipher.ENCRYPT_MODE;
|
||||
mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV);
|
||||
mCtxPtr = nInit(opmode == Cipher.ENCRYPT_MODE, key.getEncoded(), mIV);
|
||||
addToCleanupQueue(this, mCtxPtr);
|
||||
}
|
||||
|
||||
@@ -263,26 +250,23 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
protected void engineSetPadding(String padding)
|
||||
throws NoSuchPaddingException {
|
||||
|
||||
if ( ! mIsInited ) {
|
||||
NativeLib.init();
|
||||
if ( !mIsInit) {
|
||||
NativeLib.INSTANCE.init();
|
||||
}
|
||||
|
||||
if ( padding.length() == 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! padding.equals("PKCS5Padding") ) {
|
||||
if ( !padding.equals("PKCS5Padding") ) {
|
||||
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
|
||||
}
|
||||
|
||||
mPadding = true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
|
||||
int maxSize = engineGetOutputSize(inputLen);
|
||||
byte output[] = new byte[maxSize];
|
||||
byte[] output = new byte[maxSize];
|
||||
|
||||
int updateSize = update(input, inputOffset, inputLen, output, 0);
|
||||
|
||||
@@ -302,24 +286,15 @@ public class NativeAESCipherSpi extends CipherSpi {
|
||||
byte[] output, int outputOffset) throws ShortBufferException {
|
||||
|
||||
int result = update(input, inputOffset, inputLen, output, outputOffset);
|
||||
|
||||
if ( result == -1 ) {
|
||||
throw new ShortBufferException("Insufficient buffer.");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
|
||||
private int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
|
||||
int outputSize = engineGetOutputSize(inputLen);
|
||||
|
||||
int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
|
||||
|
||||
|
||||
return out;
|
||||
|
||||
|
||||
return nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
|
||||
}
|
||||
|
||||
private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
@@ -17,30 +17,30 @@
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto;
|
||||
package com.kunzisoft.keepass.crypto
|
||||
|
||||
public class NativeLib {
|
||||
private static boolean isLoaded = false;
|
||||
private static boolean loadSuccess = false;
|
||||
object NativeLib {
|
||||
private var isLoaded = false
|
||||
private var loadSuccess = false
|
||||
|
||||
public static boolean loaded() {
|
||||
return init();
|
||||
fun loaded(): Boolean {
|
||||
return init()
|
||||
}
|
||||
|
||||
public static boolean init() {
|
||||
if ( ! isLoaded ) {
|
||||
fun init(): Boolean {
|
||||
if (!isLoaded) {
|
||||
try {
|
||||
System.loadLibrary("final-key");
|
||||
System.loadLibrary("argon2");
|
||||
} catch ( UnsatisfiedLinkError e) {
|
||||
return false;
|
||||
}
|
||||
isLoaded = true;
|
||||
loadSuccess = true;
|
||||
System.loadLibrary("final-key")
|
||||
System.loadLibrary("argon2")
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
return false
|
||||
}
|
||||
|
||||
return loadSuccess;
|
||||
isLoaded = true
|
||||
loadSuccess = true
|
||||
}
|
||||
|
||||
return loadSuccess
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto;
|
||||
|
||||
import org.spongycastle.crypto.StreamCipher;
|
||||
import org.spongycastle.crypto.engines.ChaCha7539Engine;
|
||||
import org.spongycastle.crypto.engines.Salsa20Engine;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import org.spongycastle.crypto.params.ParametersWithIV;
|
||||
|
||||
public class PwStreamCipherFactory {
|
||||
public static StreamCipher getInstance(CrsAlgorithm alg, byte[] key) {
|
||||
if ( alg == CrsAlgorithm.Salsa20 ) {
|
||||
return getSalsa20(key);
|
||||
} else if (alg == CrsAlgorithm.ChaCha20) {
|
||||
return getChaCha20(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final byte[] SALSA_IV = new byte[]{ (byte)0xE8, 0x30, 0x09, 0x4B,
|
||||
(byte)0x97, 0x20, 0x5D, 0x2A };
|
||||
|
||||
private static StreamCipher getSalsa20(byte[] key) {
|
||||
// Build stream cipher key
|
||||
byte[] key32 = CryptoUtil.hashSha256(key);
|
||||
|
||||
KeyParameter keyParam = new KeyParameter(key32);
|
||||
ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV);
|
||||
|
||||
StreamCipher cipher = new Salsa20Engine();
|
||||
cipher.init(true, ivParam);
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static StreamCipher getChaCha20(byte[] key) {
|
||||
// Build stream cipher key
|
||||
byte[] hash = CryptoUtil.hashSha512(key);
|
||||
byte[] key32 = new byte[32];
|
||||
byte[] iv = new byte[12];
|
||||
|
||||
System.arraycopy(hash, 0, key32, 0, 32);
|
||||
System.arraycopy(hash, 32, iv, 0, 12);
|
||||
|
||||
KeyParameter keyParam = new KeyParameter(key32);
|
||||
ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv);
|
||||
|
||||
StreamCipher cipher = new ChaCha7539Engine();
|
||||
cipher.init(true, ivParam);
|
||||
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto
|
||||
|
||||
import org.spongycastle.crypto.StreamCipher
|
||||
import org.spongycastle.crypto.engines.ChaCha7539Engine
|
||||
import org.spongycastle.crypto.engines.Salsa20Engine
|
||||
import org.spongycastle.crypto.params.KeyParameter
|
||||
import org.spongycastle.crypto.params.ParametersWithIV
|
||||
|
||||
object StreamCipherFactory {
|
||||
|
||||
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
|
||||
|
||||
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher? {
|
||||
return when {
|
||||
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
|
||||
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSalsa20(key: ByteArray): StreamCipher {
|
||||
// Build stream cipher key
|
||||
val key32 = CryptoUtil.hashSha256(key)
|
||||
|
||||
val keyParam = KeyParameter(key32)
|
||||
val ivParam = ParametersWithIV(keyParam, SALSA_IV)
|
||||
|
||||
val cipher = Salsa20Engine()
|
||||
cipher.init(true, ivParam)
|
||||
|
||||
return cipher
|
||||
}
|
||||
|
||||
private fun getChaCha20(key: ByteArray): StreamCipher {
|
||||
// Build stream cipher key
|
||||
val hash = CryptoUtil.hashSha512(key)
|
||||
val key32 = ByteArray(32)
|
||||
val iv = ByteArray(12)
|
||||
|
||||
System.arraycopy(hash, 0, key32, 0, 32)
|
||||
System.arraycopy(hash, 32, iv, 0, 12)
|
||||
|
||||
val keyParam = KeyParameter(key32)
|
||||
val ivParam = ParametersWithIV(keyParam, iv)
|
||||
|
||||
val cipher = ChaCha7539Engine()
|
||||
cipher.init(true, ivParam)
|
||||
|
||||
return cipher
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine;
|
||||
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AesEngine extends CipherEngine {
|
||||
|
||||
public static final UUID CIPHER_UUID = Types.bytestoUUID(
|
||||
new byte[]{(byte) 0x31, (byte) 0xC1, (byte) 0xF2, (byte) 0xE6, (byte) 0xBF, (byte) 0x71, (byte) 0x43, (byte) 0x50,
|
||||
(byte) 0xBE, (byte) 0x58, (byte) 0x05, (byte) 0x21, (byte) 0x6A, (byte) 0xFC, (byte)0x5A, (byte) 0xFF
|
||||
});
|
||||
|
||||
@Override
|
||||
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
Cipher cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride);
|
||||
|
||||
cipher.init(opmode, new SecretKeySpec(key, "AES"), new IvParameterSpec(IV));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwEncryptionAlgorithm getPwEncryptionAlgorithm() {
|
||||
return PwEncryptionAlgorithm.AESRijndael;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class AesEngine : CipherEngine() {
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
||||
val cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride)
|
||||
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
|
||||
return cipher
|
||||
}
|
||||
|
||||
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
|
||||
return PwEncryptionAlgorithm.AESRijndael
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0x31.toByte(), 0xC1.toByte(), 0xF2.toByte(), 0xE6.toByte(), 0xBF.toByte(), 0x71.toByte(), 0x43.toByte(), 0x50.toByte(), 0xBE.toByte(), 0x58.toByte(), 0x05.toByte(), 0x21.toByte(), 0x6A.toByte(), 0xFC.toByte(), 0x5A.toByte(), 0xFF.toByte()))
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine;
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class ChaCha20Engine extends CipherEngine {
|
||||
|
||||
public static final UUID CIPHER_UUID = Types.bytestoUUID(
|
||||
new byte[]{(byte)0xD6, (byte)0x03, (byte)0x8A, (byte)0x2B, (byte)0x8B, (byte)0x6F,
|
||||
(byte)0x4C, (byte)0xB5, (byte)0xA5, (byte)0x24, (byte)0x33, (byte)0x9A, (byte)0x31,
|
||||
(byte)0xDB, (byte)0xB5, (byte)0x9A});
|
||||
|
||||
@Override
|
||||
public int ivLength() {
|
||||
return 12;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
Cipher cipher = Cipher.getInstance("Chacha7539", new BouncyCastleProvider());
|
||||
cipher.init(opmode, new SecretKeySpec(key, "ChaCha7539"), new IvParameterSpec(IV));
|
||||
return cipher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwEncryptionAlgorithm getPwEncryptionAlgorithm() {
|
||||
return PwEncryptionAlgorithm.ChaCha20;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class ChaCha20Engine : CipherEngine() {
|
||||
|
||||
override fun ivLength(): Int {
|
||||
return 12
|
||||
}
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
||||
val cipher = Cipher.getInstance("Chacha7539", BouncyCastleProvider())
|
||||
cipher.init(opmode, SecretKeySpec(key, "ChaCha7539"), IvParameterSpec(IV))
|
||||
return cipher
|
||||
}
|
||||
|
||||
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
|
||||
return PwEncryptionAlgorithm.ChaCha20
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0xD6.toByte(), 0x03.toByte(), 0x8A.toByte(), 0x2B.toByte(), 0x8B.toByte(), 0x6F.toByte(), 0x4C.toByte(), 0xB5.toByte(), 0xA5.toByte(), 0x24.toByte(), 0x33.toByte(), 0x9A.toByte(), 0x31.toByte(), 0xDB.toByte(), 0xB5.toByte(), 0x9A.toByte()))
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine;
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
public abstract class CipherEngine {
|
||||
public int keyLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public int ivLength() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
public abstract Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException;
|
||||
|
||||
public Cipher getCipher(int opmode, byte[] key, byte[] IV) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
return getCipher(opmode, key, IV, false);
|
||||
}
|
||||
|
||||
public abstract PwEncryptionAlgorithm getPwEncryptionAlgorithm();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
|
||||
abstract class CipherEngine {
|
||||
|
||||
fun keyLength(): Int {
|
||||
return 32
|
||||
}
|
||||
|
||||
open fun ivLength(): Int {
|
||||
return 16
|
||||
}
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||
return getCipher(opmode, key, IV, false)
|
||||
}
|
||||
|
||||
abstract fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class TwofishEngine extends CipherEngine {
|
||||
|
||||
public static final UUID CIPHER_UUID = Types.bytestoUUID(
|
||||
new byte[]{(byte)0xAD, (byte)0x68, (byte)0xF2, (byte)0x9F, (byte)0x57, (byte)0x6F, (byte)0x4B, (byte)0xB9,
|
||||
(byte)0xA3, (byte)0x6A, (byte)0xD4, (byte)0x7A, (byte)0xF9, (byte)0x65, (byte)0x34, (byte)0x6C
|
||||
});
|
||||
@Override
|
||||
public Cipher getCipher(int opmode, byte[] key, byte[] IV, boolean androidOverride) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
Cipher cipher;
|
||||
if (opmode == Cipher.ENCRYPT_MODE) {
|
||||
cipher = CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride);
|
||||
} else {
|
||||
cipher = CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride);
|
||||
}
|
||||
|
||||
cipher.init(opmode, new SecretKeySpec(key, "AES"), new IvParameterSpec(IV));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwEncryptionAlgorithm getPwEncryptionAlgorithm() {
|
||||
return PwEncryptionAlgorithm.Twofish;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.engine
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.UUID
|
||||
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class TwofishEngine : CipherEngine() {
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
||||
val cipher: Cipher = if (opmode == Cipher.ENCRYPT_MODE) {
|
||||
CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride)
|
||||
} else {
|
||||
CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride)
|
||||
}
|
||||
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
|
||||
|
||||
return cipher
|
||||
}
|
||||
|
||||
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
|
||||
return PwEncryptionAlgorithm.Twofish
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
||||
byteArrayOf(0xAD.toByte(), 0x68.toByte(), 0xF2.toByte(), 0x9F.toByte(), 0x57.toByte(), 0x6F.toByte(), 0x4B.toByte(), 0xB9.toByte(), 0xA3.toByte(), 0x6A.toByte(), 0xD4.toByte(), 0x7A.toByte(), 0xF9.toByte(), 0x65.toByte(), 0x34.toByte(), 0x6C.toByte()))
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,8 @@ import com.kunzisoft.keepass.crypto.CipherFactory;
|
||||
|
||||
public class FinalKeyFactory {
|
||||
public static FinalKey createFinalKey() {
|
||||
return createFinalKey(false);
|
||||
}
|
||||
|
||||
public static FinalKey createFinalKey(boolean androidOverride) {
|
||||
// Prefer the native final key implementation
|
||||
if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) {
|
||||
if ( !CipherFactory.INSTANCE.deviceBlacklisted() && NativeFinalKey.availble() ) {
|
||||
return new NativeFinalKey();
|
||||
} else {
|
||||
// Fall back on the android crypto implementation
|
||||
|
||||
@@ -27,12 +27,12 @@ import java.io.IOException;
|
||||
public class NativeFinalKey extends FinalKey {
|
||||
|
||||
public static boolean availble() {
|
||||
return NativeLib.init();
|
||||
return NativeLib.INSTANCE.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
|
||||
NativeLib.init();
|
||||
NativeLib.INSTANCE.init();
|
||||
|
||||
return nTransformMasterKey(seed, key, rounds);
|
||||
|
||||
|
||||
@@ -71,11 +71,11 @@ public class AesKdf extends KdfEngine {
|
||||
byte[] seed = p.getByteArray(ParamSeed);
|
||||
|
||||
if (masterKey.length != 32) {
|
||||
masterKey = CryptoUtil.hashSha256(masterKey);
|
||||
masterKey = CryptoUtil.INSTANCE.hashSha256(masterKey);
|
||||
}
|
||||
|
||||
if (seed.length != 32) {
|
||||
seed = CryptoUtil.hashSha256(seed);
|
||||
seed = CryptoUtil.INSTANCE.hashSha256(seed);
|
||||
}
|
||||
|
||||
FinalKey key = FinalKeyFactory.createFinalKey();
|
||||
|
||||
@@ -28,7 +28,7 @@ public class Argon2Native {
|
||||
public static byte[] transformKey(byte[] password, byte[] salt, int parallelism,
|
||||
long memory, long iterations, byte[] secretKey,
|
||||
byte[] associatedData, long version) throws IOException {
|
||||
NativeLib.init();
|
||||
NativeLib.INSTANCE.init();
|
||||
|
||||
return nTransformMasterKey(password, salt, parallelism, memory, iterations, secretKey, associatedData, version);
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.io.IOException
|
||||
|
||||
class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
context: Context,
|
||||
database: Database,
|
||||
withMasterPassword: Boolean,
|
||||
@@ -57,7 +57,7 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
mBackupKey = ByteArray(database.masterKey.size)
|
||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||
|
||||
val uriInputStream = getUriInputStream(context.contentResolver, mKeyFile)
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
|
||||
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
||||
|
||||
// To save the database
|
||||
@@ -72,8 +72,8 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
||||
if (!isSuccess) {
|
||||
override fun onFinishRun(result: Result) {
|
||||
if (!result.isSuccess) {
|
||||
// Erase the current master key
|
||||
erase(database.masterKey)
|
||||
mBackupKey?.let {
|
||||
@@ -81,7 +81,7 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
super.onFinishRun(isSuccess, message)
|
||||
super.onFinishRun(result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,30 +19,40 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class CreateDatabaseRunnable(private val mFilename: String,
|
||||
val onDatabaseCreate: (database: Database) -> ActionRunnable)
|
||||
: ActionRunnable() {
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabaseUri: Uri,
|
||||
private val mDatabase: Database,
|
||||
withMasterPassword: Boolean,
|
||||
masterPassword: String?,
|
||||
withKeyFile: Boolean,
|
||||
keyFile: Uri?,
|
||||
save: Boolean,
|
||||
actionRunnable: ActionRunnable? = null)
|
||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, withMasterPassword, masterPassword, withKeyFile, keyFile, save, actionRunnable) {
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
// Create new database record
|
||||
Database(mFilename).apply {
|
||||
App.currentDatabase = this
|
||||
mDatabase.apply {
|
||||
createData(mDatabaseUri)
|
||||
// Set Database state
|
||||
loaded = true
|
||||
// Commit changes
|
||||
onDatabaseCreate(this).run()
|
||||
super.run()
|
||||
}
|
||||
|
||||
finishRun(true)
|
||||
} catch (e: Exception) {
|
||||
|
||||
mDatabase.closeAndClear()
|
||||
finishRun(false, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {}
|
||||
override fun onFinishRun(result: Result) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
|
||||
class DatabaseTaskNotificationService : Service() {
|
||||
|
||||
private var notificationManager: NotificationManager? = null
|
||||
private val notificationId = 532
|
||||
|
||||
private var colorNotificationAccent: Int = 0
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Create notification channel for Oreo+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(CHANNEL_ID_DATABASE_TASK,
|
||||
CHANNEL_NAME_DATABASE_TASK,
|
||||
NotificationManager.IMPORTANCE_LOW)
|
||||
notificationManager?.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
// Get the color
|
||||
setTheme(Stylish.getThemeId(this))
|
||||
val typedValue = TypedValue()
|
||||
val theme = theme
|
||||
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true)
|
||||
colorNotificationAccent = typedValue.data
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) {
|
||||
Log.w(TAG, "null intent")
|
||||
} else {
|
||||
newNotification(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, R.string.saving_database))
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun newNotification(title: Int) {
|
||||
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID_DATABASE_TASK)
|
||||
.setSmallIcon(R.drawable.ic_data_usage_white_24dp)
|
||||
.setColor(colorNotificationAccent)
|
||||
.setContentTitle(getString(title))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
|
||||
//.setContentText(getString(R.string.saving_database))
|
||||
.setAutoCancel(false)
|
||||
.setContentIntent(null)
|
||||
startForeground(notificationId, builder.build())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
notificationManager?.cancel(notificationId)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DatabaseTaskNotificationService::class.java.name
|
||||
|
||||
const val DATABASE_TASK_TITLE_KEY = "DatabaseTaskTitle"
|
||||
|
||||
private const val CHANNEL_ID_DATABASE_TASK = "com.kunzisoft.database.notification.task.channel"
|
||||
private const val CHANNEL_NAME_DATABASE_TASK = "Database task notification"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -122,7 +122,11 @@ class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
|
||||
FileDatabaseHistory.getInstance(mWeakContext).addDatabaseUri(uri, keyFileUri)
|
||||
}
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {}
|
||||
override fun onFinishRun(result: Result) {
|
||||
if (!result.isSuccess) {
|
||||
mDatabase.closeAndClear(mWeakContext.get()?.filesDir)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = LoadDatabaseRunnable::class.java.name
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.support.annotation.StringRes
|
||||
import android.support.v4.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskNotificationService.Companion.DATABASE_TASK_TITLE_KEY
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
open class ProgressDialogThread(private val activity: FragmentActivity,
|
||||
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
|
||||
@@ -18,18 +22,31 @@ open class ProgressDialogThread(private val activity: FragmentActivity,
|
||||
messageId,
|
||||
warningId)
|
||||
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
|
||||
var actionFinishInUIThread: ActionRunnable? = null
|
||||
|
||||
private var intentDatabaseTask:Intent = Intent(activity, DatabaseTaskNotificationService::class.java)
|
||||
|
||||
init {
|
||||
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
|
||||
{
|
||||
activity.runOnUiThread {
|
||||
intentDatabaseTask.putExtra(DATABASE_TASK_TITLE_KEY, titleId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intentDatabaseTask)
|
||||
} else {
|
||||
activity.startService(intentDatabaseTask)
|
||||
}
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
// Show the dialog
|
||||
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
|
||||
}
|
||||
}, {
|
||||
}, { result ->
|
||||
activity.runOnUiThread {
|
||||
actionFinishInUIThread?.onFinishRun(result)
|
||||
// Remove the progress task
|
||||
ProgressTaskDialogFragment.stop(activity)
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
|
||||
activity.stopService(intentDatabaseTask)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -41,24 +58,28 @@ open class ProgressDialogThread(private val activity: FragmentActivity,
|
||||
|
||||
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
|
||||
private val onPreExecute: () -> Unit,
|
||||
private val onPostExecute: () -> Unit)
|
||||
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, Void>() {
|
||||
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
|
||||
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() {
|
||||
|
||||
override fun onPreExecute() {
|
||||
super.onPreExecute()
|
||||
onPreExecute.invoke()
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): Void? {
|
||||
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
|
||||
var resultTask = ActionRunnable.Result(false)
|
||||
actionRunnables.forEach {
|
||||
it?.invoke(progressTaskUpdater)?.run()
|
||||
it?.invoke(progressTaskUpdater)?.apply {
|
||||
run()
|
||||
resultTask = result
|
||||
}
|
||||
return null
|
||||
}
|
||||
return resultTask
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Void?) {
|
||||
override fun onPostExecute(result: ActionRunnable.Result) {
|
||||
super.onPostExecute(result)
|
||||
onPostExecute.invoke()
|
||||
onPostExecute.invoke(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,23 +20,16 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
abstract class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private val save: Boolean,
|
||||
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
|
||||
|
||||
init {
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
}
|
||||
|
||||
// TODO Service to prevent background thread kill
|
||||
override fun run() {
|
||||
if (save) {
|
||||
@@ -52,8 +45,19 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
// Need to call super.run() in child class
|
||||
}
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
||||
// Need to call super.onFinishRun(isSuccess, message) in child class
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(context)
|
||||
override fun onFinishRun(result: Result) {
|
||||
// Need to call super.onFinishRun(result) in child class
|
||||
}
|
||||
}
|
||||
|
||||
class SaveDatabaseActionRunnable(context: Context,
|
||||
database: Database,
|
||||
save: Boolean,
|
||||
nestedAction: ActionRunnable? = null)
|
||||
: SaveDatabaseRunnable(context, database, save, nestedAction) {
|
||||
|
||||
override fun run() {
|
||||
super.run()
|
||||
finishRun(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,17 +30,17 @@ abstract class ActionNodeDatabaseRunnable(
|
||||
/**
|
||||
* Function do get the finish node action, don't implements onFinishRun() if used this
|
||||
*/
|
||||
abstract fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues
|
||||
abstract fun nodeFinish(result: Result): ActionNodeValues
|
||||
|
||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
||||
override fun onFinishRun(result: Result) {
|
||||
callbackRunnable?.apply {
|
||||
onActionNodeFinish(nodeFinish(isSuccess, message))
|
||||
onActionNodeFinish(nodeFinish(result))
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
if (!result.isSuccess) {
|
||||
displayMessage(context)
|
||||
}
|
||||
|
||||
super.onFinishRun(isSuccess, message)
|
||||
super.onFinishRun(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ class AddEntryRunnable constructor(
|
||||
database.addEntryTo(mNewEntry, mParent)
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
mNewEntry.parent?.let {
|
||||
database.removeEntryFrom(mNewEntry, it)
|
||||
}
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, null, mNewEntry)
|
||||
return ActionNodeValues(result, null, mNewEntry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ class AddGroupRunnable constructor(
|
||||
database.addGroupTo(mNewGroup, mParent)
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
database.removeGroupFrom(mNewGroup, mParent)
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, null, mNewGroup)
|
||||
return ActionNodeValues(result, null, mNewGroup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
/**
|
||||
* Callback method who return the node(s) modified after an action
|
||||
@@ -29,7 +30,7 @@ import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
* - Move : @param oldNode NULL, @param NodeToMove
|
||||
* - Update : @param oldNode NodeToUpdate, @param NodeUpdated
|
||||
*/
|
||||
data class ActionNodeValues(val success: Boolean, val message: String?, val oldNode: NodeVersioned?, val newNode: NodeVersioned?)
|
||||
data class ActionNodeValues(val result: ActionRunnable.Result, val oldNode: NodeVersioned?, val newNode: NodeVersioned?)
|
||||
|
||||
abstract class AfterActionNodeFinishRunnable {
|
||||
abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)
|
||||
|
||||
@@ -46,8 +46,8 @@ class CopyEntryRunnable constructor(
|
||||
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to delete the copy
|
||||
try {
|
||||
mEntryCopied?.let {
|
||||
@@ -58,7 +58,7 @@ class CopyEntryRunnable constructor(
|
||||
}
|
||||
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, mEntryToCopy, mEntryCopied)
|
||||
return ActionNodeValues(result, mEntryToCopy, mEntryCopied)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -33,7 +33,7 @@ class DeleteEntryRunnable constructor(
|
||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||
|
||||
private var mParent: GroupVersioned? = null
|
||||
private var mRecycle: Boolean = false
|
||||
private var mCanRecycle: Boolean = false
|
||||
|
||||
|
||||
override fun nodeAction() {
|
||||
@@ -41,24 +41,24 @@ class DeleteEntryRunnable constructor(
|
||||
mParent?.touch(modified = false, touchParents = true)
|
||||
|
||||
// Remove Entry from parent
|
||||
mRecycle = database.canRecycle(mEntryToDelete)
|
||||
if (mRecycle) {
|
||||
database.recycle(mEntryToDelete)
|
||||
mCanRecycle = database.canRecycle(mEntryToDelete)
|
||||
if (mCanRecycle) {
|
||||
database.recycle(mEntryToDelete, context.resources)
|
||||
} else {
|
||||
database.deleteEntry(mEntryToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
mParent?.let {
|
||||
if (mRecycle) {
|
||||
if (mCanRecycle) {
|
||||
database.undoRecycle(mEntryToDelete, it)
|
||||
} else {
|
||||
database.undoDeleteEntry(mEntryToDelete, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, mEntryToDelete, null)
|
||||
return ActionNodeValues(result, mEntryToDelete, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ class DeleteGroupRunnable(context: FragmentActivity,
|
||||
// Remove Group from parent
|
||||
mRecycle = database.canRecycle(mGroupToDelete)
|
||||
if (mRecycle) {
|
||||
database.recycle(mGroupToDelete)
|
||||
database.recycle(mGroupToDelete, context.resources)
|
||||
} else {
|
||||
database.deleteGroup(mGroupToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
if (mRecycle) {
|
||||
mParent?.let {
|
||||
database.undoRecycle(mGroupToDelete, it)
|
||||
@@ -56,6 +56,6 @@ class DeleteGroupRunnable(context: FragmentActivity,
|
||||
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
|
||||
}
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, mGroupToDelete, null)
|
||||
return ActionNodeValues(result, mGroupToDelete, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ class MoveEntryRunnable constructor(
|
||||
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to remove in the first place
|
||||
try {
|
||||
if (mEntryToMove != null && mOldParent != null)
|
||||
@@ -56,7 +56,7 @@ class MoveEntryRunnable constructor(
|
||||
}
|
||||
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, null, mEntryToMove)
|
||||
return ActionNodeValues(result, null, mEntryToMove)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -53,8 +53,8 @@ class MoveGroupRunnable constructor(
|
||||
} ?: Log.e(TAG, "Unable to create a copy of the group")
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, try to move in the first place
|
||||
try {
|
||||
if (mGroupToMove != null && mOldParent != null)
|
||||
@@ -64,7 +64,7 @@ class MoveGroupRunnable constructor(
|
||||
}
|
||||
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, null, mGroupToMove)
|
||||
return ActionNodeValues(result, null, mGroupToMove)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -42,13 +42,13 @@ class UpdateEntryRunnable constructor(
|
||||
mOldEntry.updateWith(mNewEntry)
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
mBackupEntry?.let {
|
||||
mOldEntry.updateWith(it)
|
||||
}
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, mOldEntry, mNewEntry)
|
||||
return ActionNodeValues(result, mOldEntry, mNewEntry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ class UpdateGroupRunnable constructor(
|
||||
mOldGroup.updateWith(mNewGroup)
|
||||
}
|
||||
|
||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
||||
if (!isSuccess) {
|
||||
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||
if (!result.isSuccess) {
|
||||
// If we fail to save, back out changes to global structure
|
||||
mOldGroup.updateWith(mBackupGroup)
|
||||
}
|
||||
return ActionNodeValues(isSuccess, message, mOldGroup, mNewGroup)
|
||||
return ActionNodeValues(result, mOldGroup, mNewGroup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -43,9 +43,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
@@ -60,8 +58,6 @@ class Database {
|
||||
private var mUri: Uri? = null
|
||||
private var searchHelper: SearchDbHelper? = null
|
||||
var isReadOnly = false
|
||||
var isPasswordEncodingError = false
|
||||
private set
|
||||
|
||||
val drawFactory = IconDrawableFactory()
|
||||
|
||||
@@ -69,7 +65,7 @@ class Database {
|
||||
|
||||
val iconFactory: PwIconFactory
|
||||
get() {
|
||||
return pwDatabaseV3?.getIconFactory() ?: pwDatabaseV4?.getIconFactory() ?: PwIconFactory()
|
||||
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
|
||||
}
|
||||
|
||||
val name: String
|
||||
@@ -110,13 +106,13 @@ class Database {
|
||||
return ArrayList()
|
||||
}
|
||||
|
||||
val kdfEngine: KdfEngine?
|
||||
val kdfEngine: KdfEngine
|
||||
get() {
|
||||
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
|
||||
}
|
||||
|
||||
val numberKeyEncryptionRoundsAsString: String
|
||||
get() = java.lang.Long.toString(numberKeyEncryptionRounds)
|
||||
get() = numberKeyEncryptionRounds.toString()
|
||||
|
||||
var numberKeyEncryptionRounds: Long
|
||||
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
|
||||
@@ -127,7 +123,7 @@ class Database {
|
||||
}
|
||||
|
||||
val memoryUsageAsString: String
|
||||
get() = java.lang.Long.toString(memoryUsage)
|
||||
get() = memoryUsage.toString()
|
||||
|
||||
var memoryUsage: Long
|
||||
get() {
|
||||
@@ -138,7 +134,7 @@ class Database {
|
||||
}
|
||||
|
||||
val parallelismAsString: String
|
||||
get() = Integer.toString(parallelism)
|
||||
get() = parallelism.toString()
|
||||
|
||||
var parallelism: Int
|
||||
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOW_VALUE
|
||||
@@ -147,7 +143,7 @@ class Database {
|
||||
}
|
||||
|
||||
var masterKey: ByteArray
|
||||
get() = pwDatabaseV3?.getMasterKey() ?: pwDatabaseV4?.getMasterKey() ?: ByteArray(32)
|
||||
get() = pwDatabaseV3?.masterKey ?: pwDatabaseV4?.masterKey ?: ByteArray(32)
|
||||
set(masterKey) {
|
||||
pwDatabaseV3?.masterKey = masterKey
|
||||
pwDatabaseV4?.masterKey = masterKey
|
||||
@@ -155,11 +151,11 @@ class Database {
|
||||
|
||||
val rootGroup: GroupVersioned?
|
||||
get() {
|
||||
pwDatabaseV3?.let {
|
||||
return GroupVersioned(it.rootGroup)
|
||||
pwDatabaseV3?.rootGroup?.let {
|
||||
return GroupVersioned(it)
|
||||
}
|
||||
pwDatabaseV4?.let {
|
||||
return GroupVersioned(it.rootGroup)
|
||||
pwDatabaseV4?.rootGroup?.let {
|
||||
return GroupVersioned(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -182,24 +178,6 @@ class Database {
|
||||
return null
|
||||
}
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(databasePath: String) {
|
||||
// TODO Test with kdb extension
|
||||
if (isKDBExtension(databasePath)) {
|
||||
setDatabaseV3(PwDatabaseV3())
|
||||
} else {
|
||||
val databaseV4 = PwDatabaseV4()
|
||||
val groupV4 = databaseV4.createGroup()
|
||||
groupV4.title = dbNameFromPath(databasePath)
|
||||
groupV4.icon = databaseV4.getIconFactory().folderIcon
|
||||
databaseV4.rootGroup = groupV4
|
||||
setDatabaseV4(databaseV4)
|
||||
}
|
||||
|
||||
setUri(UriUtil.parseDefaultFile(databasePath))
|
||||
}
|
||||
|
||||
private fun setDatabaseV3(pwDatabaseV3: PwDatabaseV3) {
|
||||
this.pwDatabaseV3 = pwDatabaseV3
|
||||
this.pwDatabaseV4 = null
|
||||
@@ -210,17 +188,9 @@ class Database {
|
||||
this.pwDatabaseV4 = pwDatabaseV4
|
||||
}
|
||||
|
||||
private fun isKDBExtension(filename: String?): Boolean {
|
||||
if (filename == null) {
|
||||
return false
|
||||
}
|
||||
val extIdx = filename.lastIndexOf(".")
|
||||
return if (extIdx == -1) false else filename.substring(extIdx).equals(".kdb", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun dbNameFromPath(dbPath: String): String {
|
||||
val filename = URLUtil.guessFileName(dbPath, null, null)
|
||||
if (EmptyUtils.isNullOrEmpty(filename)) {
|
||||
private fun dbNameFromUri(databaseUri: Uri): String {
|
||||
val filename = URLUtil.guessFileName(databaseUri.path, null, null)
|
||||
if (filename == null || filename.isEmpty()) {
|
||||
return "KeePass Database"
|
||||
}
|
||||
val lastExtDot = filename.lastIndexOf(".")
|
||||
@@ -229,8 +199,10 @@ class Database {
|
||||
} else filename.substring(0, lastExtDot)
|
||||
}
|
||||
|
||||
fun setUri(mUri: Uri) {
|
||||
this.mUri = mUri
|
||||
fun createData(databaseUri: Uri) {
|
||||
// Always create a new database with the last version
|
||||
setDatabaseV4(PwDatabaseV4(dbNameFromUri(databaseUri)))
|
||||
this.mUri = databaseUri
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidDBException::class)
|
||||
@@ -246,7 +218,7 @@ class Database {
|
||||
// Pass Uris as InputStreams
|
||||
val inputStream: InputStream?
|
||||
try {
|
||||
inputStream = getUriInputStream(ctx.contentResolver, uri)
|
||||
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw ContentFileNotFoundException.getInstance(uri)
|
||||
@@ -256,7 +228,7 @@ class Database {
|
||||
var keyFileInputStream: InputStream? = null
|
||||
keyfile?.let {
|
||||
try {
|
||||
keyFileInputStream = getUriInputStream(ctx.contentResolver, keyfile)
|
||||
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KPD", "Database::loadData", e)
|
||||
throw ContentFileNotFoundException.getInstance(keyfile)
|
||||
@@ -300,7 +272,6 @@ class Database {
|
||||
}
|
||||
|
||||
try {
|
||||
isPasswordEncodingError = !(pwDatabaseV3?.validatePasswordEncoding(password) ?: pwDatabaseV4?.validatePasswordEncoding(password) ?: true)
|
||||
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
|
||||
loaded = true
|
||||
} catch (e: Exception) {
|
||||
@@ -320,7 +291,7 @@ class Database {
|
||||
return searchHelper?.search(this, str, max)
|
||||
}
|
||||
|
||||
fun searchEntry(query: String): Cursor? {
|
||||
fun searchEntries(query: String): Cursor? {
|
||||
|
||||
var cursorV3: EntryCursorV3? = null
|
||||
var cursorV4: EntryCursorV4? = null
|
||||
@@ -348,7 +319,7 @@ class Database {
|
||||
}
|
||||
|
||||
fun getEntryFrom(cursor: Cursor): EntryVersioned? {
|
||||
val iconFactory = pwDatabaseV3?.getIconFactory() ?: pwDatabaseV4?.getIconFactory() ?: PwIconFactory()
|
||||
val iconFactory = pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
|
||||
val entry = createEntry()
|
||||
|
||||
// TODO invert field reference manager
|
||||
@@ -428,21 +399,22 @@ class Database {
|
||||
}
|
||||
|
||||
// TODO Clear database when lock broadcast is receive in backstage
|
||||
fun closeAndClear(context: Context) {
|
||||
fun closeAndClear(filesDirectory: File? = null) {
|
||||
drawFactory.clearCache()
|
||||
// Delete the cache of the database if present
|
||||
pwDatabaseV3?.clearCache()
|
||||
pwDatabaseV4?.clearCache()
|
||||
// In all cases, delete all the files in the temp dir
|
||||
try {
|
||||
FileUtils.cleanDirectory(context.filesDir)
|
||||
FileUtils.cleanDirectory(filesDirectory)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Unable to clear the directory cache.", e)
|
||||
}
|
||||
|
||||
pwDatabaseV3 = null
|
||||
pwDatabaseV4 = null
|
||||
mUri = null
|
||||
loaded = false
|
||||
isPasswordEncodingError = false
|
||||
}
|
||||
|
||||
fun getVersion(): String {
|
||||
@@ -496,14 +468,13 @@ class Database {
|
||||
}
|
||||
|
||||
fun getKeyDerivationName(resources: Resources): String {
|
||||
val kdfEngine = kdfEngine
|
||||
return if (kdfEngine != null) {
|
||||
kdfEngine.getName(resources)
|
||||
} else ""
|
||||
return kdfEngine.getName(resources)
|
||||
}
|
||||
|
||||
fun validatePasswordEncoding(key: String): Boolean {
|
||||
return pwDatabaseV3?.validatePasswordEncoding(key) ?: pwDatabaseV4?.validatePasswordEncoding(key) ?: false
|
||||
fun validatePasswordEncoding(key: String?): Boolean {
|
||||
return pwDatabaseV3?.validatePasswordEncoding(key)
|
||||
?: pwDatabaseV4?.validatePasswordEncoding(key)
|
||||
?: false
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
@@ -515,16 +486,12 @@ class Database {
|
||||
fun createEntry(): EntryVersioned? {
|
||||
pwDatabaseV3?.let { database ->
|
||||
return EntryVersioned(database.createEntry()).apply {
|
||||
database.newEntryId()?.let {
|
||||
nodeId = it
|
||||
}
|
||||
nodeId = database.newEntryId()
|
||||
}
|
||||
}
|
||||
pwDatabaseV4?.let { database ->
|
||||
return EntryVersioned(database.createEntry()).apply {
|
||||
database.newEntryId()?.let {
|
||||
nodeId = it
|
||||
}
|
||||
nodeId = database.newEntryId()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,16 +501,12 @@ class Database {
|
||||
fun createGroup(): GroupVersioned? {
|
||||
pwDatabaseV3?.let { database ->
|
||||
return GroupVersioned(database.createGroup()).apply {
|
||||
database.newGroupId()?.let {
|
||||
setNodeId(it)
|
||||
}
|
||||
setNodeId(database.newGroupId())
|
||||
}
|
||||
}
|
||||
pwDatabaseV4?.let { database ->
|
||||
return GroupVersioned(database.createGroup()).apply {
|
||||
database.newGroupId()?.let {
|
||||
setNodeId(it)
|
||||
}
|
||||
setNodeId(database.newGroupId())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,26 +534,42 @@ class Database {
|
||||
}
|
||||
|
||||
fun addEntryTo(entry: EntryVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV3?.addEntryTo(entry.pwEntryV3, parent.pwGroupV3)
|
||||
pwDatabaseV4?.addEntryTo(entry.pwEntryV4, parent.pwGroupV4)
|
||||
entry.pwEntryV3?.let { entryV3 ->
|
||||
pwDatabaseV3?.addEntryTo(entryV3, parent.pwGroupV3)
|
||||
}
|
||||
entry.pwEntryV4?.let { entryV4 ->
|
||||
pwDatabaseV4?.addEntryTo(entryV4, parent.pwGroupV4)
|
||||
}
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun removeEntryFrom(entry: EntryVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV3?.removeEntryFrom(entry.pwEntryV3, parent.pwGroupV3)
|
||||
pwDatabaseV4?.removeEntryFrom(entry.pwEntryV4, parent.pwGroupV4)
|
||||
entry.pwEntryV3?.let { entryV3 ->
|
||||
pwDatabaseV3?.removeEntryFrom(entryV3, parent.pwGroupV3)
|
||||
}
|
||||
entry.pwEntryV4?.let { entryV4 ->
|
||||
pwDatabaseV4?.removeEntryFrom(entryV4, parent.pwGroupV4)
|
||||
}
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun addGroupTo(group: GroupVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV3?.addGroupTo(group.pwGroupV3, parent.pwGroupV3)
|
||||
pwDatabaseV4?.addGroupTo(group.pwGroupV4, parent.pwGroupV4)
|
||||
group.pwGroupV3?.let { groupV3 ->
|
||||
pwDatabaseV3?.addGroupTo(groupV3, parent.pwGroupV3)
|
||||
}
|
||||
group.pwGroupV4?.let { groupV4 ->
|
||||
pwDatabaseV4?.addGroupTo(groupV4, parent.pwGroupV4)
|
||||
}
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun removeGroupFrom(group: GroupVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV3?.removeGroupFrom(group.pwGroupV3, parent.pwGroupV3)
|
||||
pwDatabaseV4?.removeGroupFrom(group.pwGroupV4, parent.pwGroupV4)
|
||||
group.pwGroupV3?.let { groupV3 ->
|
||||
pwDatabaseV3?.removeGroupFrom(groupV3, parent.pwGroupV3)
|
||||
}
|
||||
group.pwGroupV4?.let { groupV4 ->
|
||||
pwDatabaseV4?.removeGroupFrom(groupV4, parent.pwGroupV4)
|
||||
}
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
@@ -647,37 +626,65 @@ class Database {
|
||||
}
|
||||
|
||||
fun undoDeleteEntry(entry: EntryVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV3?.undoDeleteEntryFrom(entry.pwEntryV3, parent.pwGroupV3)
|
||||
pwDatabaseV4?.undoDeleteEntryFrom(entry.pwEntryV4, parent.pwGroupV4)
|
||||
entry.pwEntryV3?.let { entryV3 ->
|
||||
pwDatabaseV3?.undoDeleteEntryFrom(entryV3, parent.pwGroupV3)
|
||||
}
|
||||
entry.pwEntryV4?.let { entryV4 ->
|
||||
pwDatabaseV4?.undoDeleteEntryFrom(entryV4, parent.pwGroupV4)
|
||||
}
|
||||
}
|
||||
|
||||
fun undoDeleteGroup(group: GroupVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV3?.undoDeleteGroupFrom(group.pwGroupV3, parent.pwGroupV3)
|
||||
pwDatabaseV4?.undoDeleteGroupFrom(group.pwGroupV4, parent.pwGroupV4)
|
||||
group.pwGroupV3?.let { groupV3 ->
|
||||
pwDatabaseV3?.undoDeleteGroupFrom(groupV3, parent.pwGroupV3)
|
||||
}
|
||||
group.pwGroupV4?.let { groupV4 ->
|
||||
pwDatabaseV4?.undoDeleteGroupFrom(groupV4, parent.pwGroupV4)
|
||||
}
|
||||
}
|
||||
|
||||
fun canRecycle(entry: EntryVersioned): Boolean {
|
||||
return pwDatabaseV4?.canRecycle(entry.pwEntryV4) ?: false
|
||||
var canRecycle: Boolean? = null
|
||||
entry.pwEntryV4?.let { entryV4 ->
|
||||
canRecycle = pwDatabaseV4?.canRecycle(entryV4)
|
||||
}
|
||||
return canRecycle ?: false
|
||||
}
|
||||
|
||||
fun canRecycle(group: GroupVersioned): Boolean {
|
||||
return pwDatabaseV4?.canRecycle(group.pwGroupV4) ?: false
|
||||
var canRecycle: Boolean? = null
|
||||
group.pwGroupV4?.let { groupV4 ->
|
||||
canRecycle = pwDatabaseV4?.canRecycle(groupV4)
|
||||
}
|
||||
return canRecycle ?: false
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryVersioned) {
|
||||
pwDatabaseV4?.recycle(entry.pwEntryV4)
|
||||
fun recycle(entry: EntryVersioned, resources: Resources) {
|
||||
entry.pwEntryV4?.let {
|
||||
pwDatabaseV4?.recycle(it, resources)
|
||||
}
|
||||
}
|
||||
|
||||
fun recycle(group: GroupVersioned) {
|
||||
pwDatabaseV4?.recycle(group.pwGroupV4)
|
||||
fun recycle(group: GroupVersioned, resources: Resources) {
|
||||
group.pwGroupV4?.let {
|
||||
pwDatabaseV4?.recycle(it, resources)
|
||||
}
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: EntryVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV4?.undoRecycle(entry.pwEntryV4, parent.pwGroupV4)
|
||||
entry.pwEntryV4?.let { entryV4 ->
|
||||
parent.pwGroupV4?.let { parentV4 ->
|
||||
pwDatabaseV4?.undoRecycle(entryV4, parentV4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun undoRecycle(group: GroupVersioned, parent: GroupVersioned) {
|
||||
pwDatabaseV4?.undoRecycle(group.pwGroupV4, parent.pwGroupV4)
|
||||
group.pwGroupV4?.let { groupV4 ->
|
||||
parent.pwGroupV4?.let { parentV4 ->
|
||||
pwDatabaseV4?.undoRecycle(groupV4, parentV4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startManageEntry(entry: EntryVersioned) {
|
||||
|
||||
@@ -304,4 +304,22 @@ class GroupVersioned : NodeVersioned, PwGroupInterface<GroupVersioned, EntryVers
|
||||
fun containsCustomData(): Boolean {
|
||||
return pwGroupV4?.containsCustomData() ?: false
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as GroupVersioned
|
||||
|
||||
if (pwGroupV3 != other.pwGroupV3) return false
|
||||
if (pwGroupV4 != other.pwGroupV4) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = pwGroupV3?.hashCode() ?: 0
|
||||
result = 31 * result + (pwGroupV4?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException;
|
||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyException;
|
||||
import com.kunzisoft.keepass.utils.MemUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class PwDatabase<Group extends PwGroup<?, Group, Entry>, Entry extends PwEntry<Group, Entry>> {
|
||||
|
||||
public static final UUID UUID_ZERO = new UUID(0,0);
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
protected PwEncryptionAlgorithm algorithm;
|
||||
|
||||
protected byte[] masterKey = new byte[32];
|
||||
protected byte[] finalKey;
|
||||
|
||||
protected PwIconFactory iconFactory = new PwIconFactory();
|
||||
|
||||
protected LinkedHashMap<PwNodeId, Group> groupIndexes = new LinkedHashMap<>();
|
||||
protected LinkedHashMap<PwNodeId, Entry> entryIndexes = new LinkedHashMap<>();
|
||||
|
||||
public abstract String getVersion();
|
||||
|
||||
public PwIconFactory getIconFactory() {
|
||||
return iconFactory;
|
||||
}
|
||||
|
||||
public byte[] getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
public void setMasterKey(byte[] masterKey) {
|
||||
this.masterKey = masterKey;
|
||||
}
|
||||
|
||||
public byte[] getFinalKey() {
|
||||
return finalKey;
|
||||
}
|
||||
|
||||
protected abstract byte[] getMasterKey(@Nullable String key, @Nullable InputStream keyInputStream)
|
||||
throws InvalidKeyFileException, IOException;
|
||||
|
||||
public void retrieveMasterKey(@Nullable String key, @Nullable InputStream keyInputStream)
|
||||
throws InvalidKeyFileException, IOException {
|
||||
masterKey = getMasterKey(key, keyInputStream);
|
||||
}
|
||||
|
||||
protected byte[] getCompositeKey(@Nullable String key, @Nullable InputStream keyInputStream)
|
||||
throws InvalidKeyFileException, IOException {
|
||||
assert(key != null && keyInputStream != null);
|
||||
|
||||
byte[] fileKey = getFileKey(keyInputStream);
|
||||
|
||||
byte[] passwordKey = getPasswordKey(key);
|
||||
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("SHA-256 not supported");
|
||||
}
|
||||
|
||||
md.update(passwordKey);
|
||||
|
||||
return md.digest(fileKey);
|
||||
}
|
||||
|
||||
protected byte[] getFileKey(InputStream keyInputStream)
|
||||
throws InvalidKeyFileException, IOException {
|
||||
assert(keyInputStream != null);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
MemUtil.copyStream(keyInputStream, bos);
|
||||
byte[] keyData = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(keyData);
|
||||
byte[] key = loadXmlKeyFile(bis);
|
||||
if ( key != null ) {
|
||||
return key;
|
||||
}
|
||||
|
||||
long fileSize = keyData.length;
|
||||
if ( fileSize == 0 ) {
|
||||
throw new KeyFileEmptyException();
|
||||
} else if ( fileSize == 32 ) {
|
||||
return keyData;
|
||||
} else if ( fileSize == 64 ) {
|
||||
byte[] hex = new byte[64];
|
||||
|
||||
try {
|
||||
return hexStringToByteArray(new String(keyData));
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
}
|
||||
}
|
||||
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("SHA-256 not supported");
|
||||
}
|
||||
//SHA256Digest md = new SHA256Digest();
|
||||
byte[] buffer = new byte[2048];
|
||||
int offset = 0;
|
||||
|
||||
try {
|
||||
md.update(keyData);
|
||||
} catch (Exception e) {
|
||||
System.out.println(e.toString());
|
||||
}
|
||||
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
protected abstract byte[] loadXmlKeyFile(InputStream keyInputStream);
|
||||
|
||||
public static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public boolean validatePasswordEncoding(String key) {
|
||||
if (key == null)
|
||||
return false;
|
||||
|
||||
String encoding = getPasswordEncoding();
|
||||
|
||||
byte[] bKey;
|
||||
try {
|
||||
bKey = key.getBytes(encoding);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String reencoded;
|
||||
|
||||
try {
|
||||
reencoded = new String(bKey, encoding);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return key.equals(reencoded);
|
||||
}
|
||||
|
||||
protected abstract String getPasswordEncoding();
|
||||
|
||||
public byte[] getPasswordKey(String key) throws IOException {
|
||||
if ( key == null)
|
||||
throw new IllegalArgumentException( "Key cannot be empty." ); // TODO
|
||||
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("SHA-256 not supported");
|
||||
}
|
||||
|
||||
byte[] bKey;
|
||||
try {
|
||||
bKey = key.getBytes(getPasswordEncoding());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
assert false;
|
||||
bKey = key.getBytes();
|
||||
}
|
||||
md.update(bKey, 0, bKey.length );
|
||||
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
public abstract long getNumberKeyEncryptionRounds();
|
||||
|
||||
public abstract void setNumberKeyEncryptionRounds(long rounds) throws NumberFormatException;
|
||||
|
||||
public PwEncryptionAlgorithm getEncryptionAlgorithm() {
|
||||
if (algorithm != null)
|
||||
return algorithm;
|
||||
return PwEncryptionAlgorithm.AESRijndael;
|
||||
}
|
||||
|
||||
public void setEncryptionAlgorithm(PwEncryptionAlgorithm algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public abstract List<PwEncryptionAlgorithm> getAvailableEncryptionAlgorithms();
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Node Creation
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
public abstract PwNodeId newGroupId();
|
||||
|
||||
public abstract PwNodeId newEntryId();
|
||||
|
||||
public abstract Group createGroup();
|
||||
|
||||
public abstract Group getRootGroup();
|
||||
|
||||
public abstract Entry createEntry();
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Index Manipulation
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine if an id number is already in use
|
||||
*
|
||||
* @param id
|
||||
* ID number to check for
|
||||
* @return True if the ID is used, false otherwise
|
||||
*/
|
||||
public boolean isGroupIdUsed(PwNodeId id) {
|
||||
return groupIndexes.containsKey(id);
|
||||
}
|
||||
|
||||
public Collection<Group> getGroupIndexes() {
|
||||
return groupIndexes.values();
|
||||
}
|
||||
|
||||
public void setGroupIndexes(List<Group> groupList) {
|
||||
this.groupIndexes.clear();
|
||||
for (Group currentGroup : groupList) {
|
||||
this.groupIndexes.put(currentGroup.getNodeId(), currentGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public Group getGroupById(PwNodeId id) {
|
||||
return this.groupIndexes.get(id);
|
||||
}
|
||||
|
||||
public void addGroupIndex(Group group) {
|
||||
this.groupIndexes.put(group.getNodeId(), group);
|
||||
}
|
||||
|
||||
public void removeGroupIndex(Group group) {
|
||||
this.groupIndexes.remove(group.getNodeId());
|
||||
}
|
||||
|
||||
public int numberOfGroups() {
|
||||
return groupIndexes.size();
|
||||
}
|
||||
|
||||
public boolean isEntryIdUsed(PwNodeId id) {
|
||||
return entryIndexes.containsKey(id);
|
||||
}
|
||||
|
||||
public Collection<Entry> getEntryIndexes() {
|
||||
return entryIndexes.values();
|
||||
}
|
||||
|
||||
public Entry getEntryById(PwNodeId id) {
|
||||
return this.entryIndexes.get(id);
|
||||
}
|
||||
|
||||
public void addEntryIndex(Entry entry) {
|
||||
this.entryIndexes.put(entry.getNodeId(), entry);
|
||||
}
|
||||
|
||||
public void removeEntryIndex(Entry entry) {
|
||||
this.entryIndexes.remove(entry.getNodeId());
|
||||
}
|
||||
|
||||
public int numberOfEntries() {
|
||||
return entryIndexes.size();
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Node Manipulation
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
public void addGroupTo(Group newGroup, @Nullable Group parent) {
|
||||
// Add tree to parent tree
|
||||
if (parent != null)
|
||||
parent.addChildGroup(newGroup);
|
||||
newGroup.setParent(parent);
|
||||
addGroupIndex(newGroup);
|
||||
}
|
||||
|
||||
public void removeGroupFrom(Group groupToRemove, Group parent) {
|
||||
// Remove tree from parent tree
|
||||
if (parent != null) {
|
||||
parent.removeChildGroup(groupToRemove);
|
||||
}
|
||||
removeGroupIndex(groupToRemove);
|
||||
}
|
||||
|
||||
public void addEntryTo(Entry newEntry, @Nullable Group parent) {
|
||||
// Add entry to parent
|
||||
if (parent != null)
|
||||
parent.addChildEntry(newEntry);
|
||||
newEntry.setParent(parent);
|
||||
addEntryIndex(newEntry);
|
||||
}
|
||||
|
||||
public void removeEntryFrom(Entry entryToRemove, Group parent) {
|
||||
// Remove entry for parent
|
||||
if (parent != null) {
|
||||
parent.removeChildEntry(entryToRemove);
|
||||
}
|
||||
removeEntryIndex(entryToRemove);
|
||||
}
|
||||
|
||||
// TODO Delete group
|
||||
public void undoDeleteGroupFrom(Group group, Group origParent) {
|
||||
addGroupTo(group, origParent);
|
||||
}
|
||||
|
||||
public void undoDeleteEntryFrom(Entry entry, Group origParent) {
|
||||
addEntryTo(entry, origParent);
|
||||
}
|
||||
|
||||
public abstract boolean isBackup(Group group);
|
||||
|
||||
public boolean isGroupSearchable(Group group, boolean omitBackup) {
|
||||
return group != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyException
|
||||
import com.kunzisoft.keepass.utils.MemUtil
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.UUID
|
||||
|
||||
abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Group, Entry>> {
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
protected var algorithm: PwEncryptionAlgorithm? = null
|
||||
|
||||
var masterKey = ByteArray(32)
|
||||
var finalKey: ByteArray? = null
|
||||
protected set
|
||||
|
||||
var iconFactory = PwIconFactory()
|
||||
protected set
|
||||
|
||||
private var groupIndexes = LinkedHashMap<PwNodeId<*>, Group>()
|
||||
private var entryIndexes = LinkedHashMap<PwNodeId<*>, Entry>()
|
||||
|
||||
abstract val version: String
|
||||
|
||||
protected abstract val passwordEncoding: String
|
||||
|
||||
abstract var numberKeyEncryptionRounds: Long
|
||||
|
||||
var encryptionAlgorithm: PwEncryptionAlgorithm
|
||||
get() {
|
||||
return algorithm ?: PwEncryptionAlgorithm.AESRijndael
|
||||
}
|
||||
set(algorithm) {
|
||||
this.algorithm = algorithm
|
||||
}
|
||||
|
||||
abstract val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
|
||||
var rootGroup: Group? = null
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
masterKey = getMasterKey(key, keyInputStream)
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
|
||||
val fileKey = getFileKey(keyInputStream)
|
||||
|
||||
val passwordKey = getPasswordKey(key)
|
||||
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("SHA-256 not supported")
|
||||
}
|
||||
|
||||
md.update(passwordKey)
|
||||
|
||||
return md.digest(fileKey)
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
MemUtil.copyStream(keyInputStream, bos)
|
||||
val keyData = bos.toByteArray()
|
||||
|
||||
val bis = ByteArrayInputStream(keyData)
|
||||
val key = loadXmlKeyFile(bis)
|
||||
if (key != null) {
|
||||
return key
|
||||
}
|
||||
|
||||
val fileSize = keyData.size.toLong()
|
||||
if (fileSize == 0L) {
|
||||
throw KeyFileEmptyException()
|
||||
} else if (fileSize == 32L) {
|
||||
return keyData
|
||||
} else if (fileSize == 64L) {
|
||||
try {
|
||||
return hexStringToByteArray(String(keyData))
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
}
|
||||
}
|
||||
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("SHA-256 not supported")
|
||||
}
|
||||
|
||||
try {
|
||||
md.update(keyData)
|
||||
} catch (e: Exception) {
|
||||
println(e.toString())
|
||||
}
|
||||
|
||||
return md.digest()
|
||||
}
|
||||
|
||||
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
|
||||
|
||||
open fun validatePasswordEncoding(key: String?): Boolean {
|
||||
if (key == null)
|
||||
return false
|
||||
|
||||
val encoding = passwordEncoding
|
||||
|
||||
val bKey: ByteArray
|
||||
try {
|
||||
bKey = key.toByteArray(charset(encoding))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
|
||||
val reEncoded: String
|
||||
try {
|
||||
reEncoded = String(bKey, charset(encoding))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
return key == reEncoded
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getPasswordKey(key: String?): ByteArray {
|
||||
if (key == null)
|
||||
throw IllegalArgumentException("Key cannot be empty.") // TODO
|
||||
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("SHA-256 not supported")
|
||||
}
|
||||
|
||||
val bKey: ByteArray = try {
|
||||
key.toByteArray(charset(passwordEncoding))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
key.toByteArray()
|
||||
}
|
||||
|
||||
md.update(bKey, 0, bKey.size)
|
||||
|
||||
return md.digest()
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Node Creation
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
abstract fun newGroupId(): PwNodeId<*>
|
||||
|
||||
abstract fun newEntryId(): PwNodeId<*>
|
||||
|
||||
abstract fun createGroup(): Group
|
||||
|
||||
abstract fun createEntry(): Entry
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Index Manipulation
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
fun doForEachGroupInIndex(action: (Group) -> Unit) {
|
||||
for (group in groupIndexes) {
|
||||
action.invoke(group.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an id number is already in use
|
||||
*
|
||||
* @param id
|
||||
* ID number to check for
|
||||
* @return True if the ID is used, false otherwise
|
||||
*/
|
||||
fun isGroupIdUsed(id: PwNodeId<*>): Boolean {
|
||||
return groupIndexes.containsKey(id)
|
||||
}
|
||||
|
||||
fun getGroupIndexes(): Collection<Group> {
|
||||
return groupIndexes.values
|
||||
}
|
||||
|
||||
fun setGroupIndexes(groupList: List<Group>) {
|
||||
this.groupIndexes.clear()
|
||||
for (currentGroup in groupList) {
|
||||
this.groupIndexes[currentGroup.nodeId] = currentGroup
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupById(id: PwNodeId<*>): Group? {
|
||||
return this.groupIndexes[id]
|
||||
}
|
||||
|
||||
fun addGroupIndex(group: Group) {
|
||||
this.groupIndexes[group.nodeId] = group
|
||||
}
|
||||
|
||||
fun removeGroupIndex(group: Group) {
|
||||
this.groupIndexes.remove(group.nodeId)
|
||||
}
|
||||
|
||||
fun numberOfGroups(): Int {
|
||||
return groupIndexes.size
|
||||
}
|
||||
|
||||
fun doForEachEntryInIndex(action: (Entry) -> Unit) {
|
||||
for (entry in entryIndexes) {
|
||||
action.invoke(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEntryIdUsed(id: PwNodeId<*>): Boolean {
|
||||
return entryIndexes.containsKey(id)
|
||||
}
|
||||
|
||||
fun getEntryIndexes(): Collection<Entry> {
|
||||
return entryIndexes.values
|
||||
}
|
||||
|
||||
fun getEntryById(id: PwNodeId<*>): Entry? {
|
||||
return this.entryIndexes[id]
|
||||
}
|
||||
|
||||
fun addEntryIndex(entry: Entry) {
|
||||
this.entryIndexes[entry.nodeId] = entry
|
||||
}
|
||||
|
||||
fun removeEntryIndex(entry: Entry) {
|
||||
this.entryIndexes.remove(entry.nodeId)
|
||||
}
|
||||
|
||||
fun numberOfEntries(): Int {
|
||||
return entryIndexes.size
|
||||
}
|
||||
|
||||
open fun clearCache() {
|
||||
this.groupIndexes.clear()
|
||||
this.entryIndexes.clear()
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Node Manipulation
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
fun addGroupTo(newGroup: Group, parent: Group?) {
|
||||
// Add tree to parent tree
|
||||
parent?.addChildGroup(newGroup)
|
||||
newGroup.parent = parent
|
||||
addGroupIndex(newGroup)
|
||||
}
|
||||
|
||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||
// Remove tree from parent tree
|
||||
parent?.removeChildGroup(groupToRemove)
|
||||
removeGroupIndex(groupToRemove)
|
||||
}
|
||||
|
||||
fun addEntryTo(newEntry: Entry, parent: Group?) {
|
||||
// Add entry to parent
|
||||
parent?.addChildEntry(newEntry)
|
||||
newEntry.parent = parent
|
||||
addEntryIndex(newEntry)
|
||||
}
|
||||
|
||||
open fun removeEntryFrom(entryToRemove: Entry, parent: Group?) {
|
||||
// Remove entry for parent
|
||||
parent?.removeChildEntry(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 isBackup(group: Group): Boolean
|
||||
|
||||
open fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
|
||||
return group != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val UUID_ZERO = UUID(0, 0)
|
||||
|
||||
fun hexStringToByteArray(s: String): ByteArray {
|
||||
val len = s.length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.database.element;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKey;
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory;
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException;
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author Naomaru Itoi <nao@phoneid.org>
|
||||
* @author Bill Zwicky <wrzwicky@pobox.com>
|
||||
* @author Dominik Reichl <dominik.reichl@t-online.de>
|
||||
*/
|
||||
public class PwDatabaseV3 extends PwDatabase<PwGroupV3, PwEntryV3> {
|
||||
|
||||
private static final int DEFAULT_ENCRYPTION_ROUNDS = 300;
|
||||
|
||||
private int numKeyEncRounds;
|
||||
|
||||
protected PwGroupV3 rootGroup;
|
||||
|
||||
public PwDatabaseV3() {
|
||||
algorithm = PwEncryptionAlgorithm.AESRijndael;
|
||||
numKeyEncRounds = DEFAULT_ENCRYPTION_ROUNDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "KeePass 1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PwEncryptionAlgorithm> getAvailableEncryptionAlgorithms() {
|
||||
List<PwEncryptionAlgorithm> list = new ArrayList<>();
|
||||
list.add(PwEncryptionAlgorithm.AESRijndael);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<PwGroupV3> getRootGroups() {
|
||||
List<PwGroupV3> kids = new ArrayList<>();
|
||||
for (Map.Entry<PwNodeId, PwGroupV3> group : groupIndexes.entrySet()) {
|
||||
if (group.getValue().getLevel() == 0)
|
||||
kids.add(group.getValue());
|
||||
}
|
||||
return kids;
|
||||
}
|
||||
|
||||
private void assignGroupsChildren(PwGroupV3 parent) {
|
||||
int levelToCheck = parent.getLevel() + 1;
|
||||
boolean startFromParentPosition = false;
|
||||
for (PwGroupV3 groupToCheck: getGroupIndexes()) {
|
||||
if (getRootGroup().getNodeId().equals(parent.getNodeId())
|
||||
|| groupToCheck.getNodeId().equals(parent.getNodeId())) {
|
||||
startFromParentPosition = true;
|
||||
}
|
||||
if (startFromParentPosition) {
|
||||
if (groupToCheck.getLevel() < levelToCheck)
|
||||
break;
|
||||
else if (groupToCheck.getLevel() == levelToCheck)
|
||||
parent.addChildGroup(groupToCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assignEntriesChildren(PwGroupV3 parent) {
|
||||
for (PwEntryV3 entry : getEntryIndexes()) {
|
||||
if (entry.getParent().getNodeId().equals(parent.getNodeId()))
|
||||
parent.addChildEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void constructTreeFromIndex(PwGroupV3 currentGroup) {
|
||||
|
||||
assignGroupsChildren(currentGroup);
|
||||
assignEntriesChildren(currentGroup);
|
||||
|
||||
// set parent in child entries (normally useless but to be sure or to update parent metadata)
|
||||
for (PwEntryV3 childEntry : currentGroup.getChildEntries()) {
|
||||
childEntry.setParent(currentGroup);
|
||||
}
|
||||
// recursively construct child groups
|
||||
for (PwGroupV3 childGroup : currentGroup.getChildGroups()) {
|
||||
childGroup.setParent(currentGroup);
|
||||
constructTreeFromIndex(childGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public void constructTreeFromIndex() {
|
||||
constructTreeFromIndex(getRootGroup());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an unused random tree id
|
||||
*
|
||||
* @return new tree id
|
||||
*/
|
||||
@Override
|
||||
public PwNodeIdInt newGroupId() {
|
||||
PwNodeIdInt newId;
|
||||
do {
|
||||
newId = new PwNodeIdInt();
|
||||
} while (isGroupIdUsed(newId));
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an unused random tree id
|
||||
*
|
||||
* @return new tree id
|
||||
*/
|
||||
@Override
|
||||
public PwNodeIdUUID newEntryId() {
|
||||
PwNodeIdUUID newId;
|
||||
do {
|
||||
newId = new PwNodeIdUUID();
|
||||
} while (isEntryIdUsed(newId));
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getMasterKey(@Nullable String key, @Nullable InputStream keyInputStream)
|
||||
throws InvalidKeyFileException, IOException {
|
||||
|
||||
if (key != null && keyInputStream != null) {
|
||||
return getCompositeKey(key, keyInputStream);
|
||||
} else if (key != null) { // key.length() >= 0
|
||||
return getPasswordKey(key);
|
||||
} else if (keyInputStream != null) { // key == null
|
||||
return getFileKey(keyInputStream);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Key cannot be empty.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the master key a few times to make brute-force key-search harder
|
||||
* @throws IOException
|
||||
*/
|
||||
private static byte[] transformMasterKey( byte[] pKeySeed, byte[] pKey, long rounds ) throws IOException {
|
||||
FinalKey key = FinalKeyFactory.createFinalKey();
|
||||
|
||||
return key.transformMasterKey(pKeySeed, pKey, rounds);
|
||||
}
|
||||
|
||||
public void makeFinalKey(byte[] masterSeed, byte[] masterSeed2, long numRounds) throws IOException {
|
||||
|
||||
// Write checksum Checksum
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("SHA-256 not implemented here.");
|
||||
}
|
||||
NullOutputStream nos = new NullOutputStream();
|
||||
DigestOutputStream dos = new DigestOutputStream(nos, md);
|
||||
|
||||
byte[] transformedMasterKey = transformMasterKey(masterSeed2, masterKey, numRounds);
|
||||
dos.write(masterSeed);
|
||||
dos.write(transformedMasterKey);
|
||||
|
||||
finalKey = md.digest();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPasswordEncoding() {
|
||||
return "ISO-8859-1";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] loadXmlKeyFile(InputStream keyInputStream) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getNumberKeyEncryptionRounds() {
|
||||
return numKeyEncRounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNumberKeyEncryptionRounds(long rounds) throws NumberFormatException {
|
||||
if (rounds > Integer.MAX_VALUE || rounds < Integer.MIN_VALUE) {
|
||||
throw new NumberFormatException();
|
||||
}
|
||||
numKeyEncRounds = (int) rounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwGroupV3 createGroup() {
|
||||
return new PwGroupV3();
|
||||
}
|
||||
|
||||
public void setRootGroup(PwGroupV3 rootGroup) {
|
||||
this.rootGroup = rootGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwGroupV3 getRootGroup() {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwEntryV3 createEntry() {
|
||||
return new PwEntryV3();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBackup(PwGroupV3 group) {
|
||||
while (group != null) {
|
||||
if (group.getLevel() == 0 && group.getTitle().equalsIgnoreCase("Backup")) {
|
||||
return true;
|
||||
}
|
||||
group = group.getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroupSearchable(PwGroupV3 group, boolean omitBackup) {
|
||||
if (!super.isGroupSearchable(group, omitBackup)) {
|
||||
return false;
|
||||
}
|
||||
return !(omitBackup && isBackup(group));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Naomaru Itoi <nao></nao>@phoneid.org>
|
||||
* @author Bill Zwicky <wrzwicky></wrzwicky>@pobox.com>
|
||||
* @author Dominik Reichl <dominik.reichl></dominik.reichl>@t-online.de>
|
||||
*/
|
||||
class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
|
||||
|
||||
private var numKeyEncRounds: Int = 0
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
|
||||
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwEncryptionAlgorithm>()
|
||||
list.add(PwEncryptionAlgorithm.AESRijndael)
|
||||
return list
|
||||
}
|
||||
|
||||
val rootGroups: List<PwGroupV3>
|
||||
get() {
|
||||
val kids = ArrayList<PwGroupV3>()
|
||||
doForEachGroupInIndex { group ->
|
||||
if (group.level == 0)
|
||||
kids.add(group)
|
||||
}
|
||||
return kids
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "ISO-8859-1"
|
||||
|
||||
override var numberKeyEncryptionRounds: Long
|
||||
get() = numKeyEncRounds.toLong()
|
||||
@Throws(NumberFormatException::class)
|
||||
set(rounds) {
|
||||
if (rounds > Integer.MAX_VALUE || rounds < Integer.MIN_VALUE) {
|
||||
throw NumberFormatException()
|
||||
}
|
||||
numKeyEncRounds = rounds.toInt()
|
||||
}
|
||||
|
||||
init {
|
||||
algorithm = PwEncryptionAlgorithm.AESRijndael
|
||||
numKeyEncRounds = DEFAULT_ENCRYPTION_ROUNDS
|
||||
}
|
||||
|
||||
private fun assignGroupsChildren(parent: PwGroupV3) {
|
||||
val levelToCheck = parent.level + 1
|
||||
var startFromParentPosition = false
|
||||
for (groupToCheck in getGroupIndexes()) {
|
||||
rootGroup?.let { root ->
|
||||
if (root.nodeId == parent.nodeId || groupToCheck.nodeId == parent.nodeId) {
|
||||
startFromParentPosition = true
|
||||
}
|
||||
}
|
||||
if (startFromParentPosition) {
|
||||
if (groupToCheck.level < levelToCheck)
|
||||
break
|
||||
else if (groupToCheck.level == levelToCheck)
|
||||
parent.addChildGroup(groupToCheck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignEntriesChildren(parent: PwGroupV3) {
|
||||
for (entry in getEntryIndexes()) {
|
||||
if (entry.parent!!.nodeId == parent.nodeId)
|
||||
parent.addChildEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructTreeFromIndex(currentGroup: PwGroupV3) {
|
||||
|
||||
assignGroupsChildren(currentGroup)
|
||||
assignEntriesChildren(currentGroup)
|
||||
|
||||
// set parent in child entries (normally useless but to be sure or to update parent metadata)
|
||||
for (childEntry in currentGroup.getChildEntries()) {
|
||||
childEntry.parent = currentGroup
|
||||
}
|
||||
// recursively construct child groups
|
||||
for (childGroup in currentGroup.getChildGroups()) {
|
||||
childGroup.parent = currentGroup
|
||||
constructTreeFromIndex(childGroup)
|
||||
}
|
||||
}
|
||||
|
||||
fun constructTreeFromIndex() {
|
||||
rootGroup?.let {
|
||||
constructTreeFromIndex(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an unused random tree id
|
||||
*
|
||||
* @return new tree id
|
||||
*/
|
||||
override fun newGroupId(): PwNodeIdInt {
|
||||
var newId: PwNodeIdInt
|
||||
do {
|
||||
newId = PwNodeIdInt()
|
||||
} while (isGroupIdUsed(newId))
|
||||
|
||||
return newId
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an unused random tree id
|
||||
*
|
||||
* @return new tree id
|
||||
*/
|
||||
override fun newEntryId(): PwNodeIdUUID {
|
||||
var newId: PwNodeIdUUID
|
||||
do {
|
||||
newId = PwNodeIdUUID()
|
||||
} while (isEntryIdUsed(newId))
|
||||
|
||||
return newId
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
return if (key != null && keyInputStream != null) {
|
||||
getCompositeKey(key, keyInputStream)
|
||||
} else key?.let { // key.length() >= 0
|
||||
getPasswordKey(it)
|
||||
} ?: (keyInputStream?.let { // key == null
|
||||
getFileKey(it)
|
||||
} ?: throw IllegalArgumentException("Key cannot be empty."))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun makeFinalKey(masterSeed: ByteArray, masterSeed2: ByteArray, numRounds: Long) {
|
||||
|
||||
// Write checksum Checksum
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("SHA-256 not implemented here.")
|
||||
}
|
||||
|
||||
val nos = NullOutputStream()
|
||||
val dos = DigestOutputStream(nos, md)
|
||||
|
||||
val transformedMasterKey = transformMasterKey(masterSeed2, masterKey, numRounds)
|
||||
dos.write(masterSeed)
|
||||
dos.write(transformedMasterKey)
|
||||
|
||||
finalKey = md.digest()
|
||||
}
|
||||
|
||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun createGroup(): PwGroupV3 {
|
||||
return PwGroupV3()
|
||||
}
|
||||
|
||||
override fun createEntry(): PwEntryV3 {
|
||||
return PwEntryV3()
|
||||
}
|
||||
|
||||
override fun isBackup(group: PwGroupV3): Boolean {
|
||||
var currentGroup: PwGroupV3? = group
|
||||
while (currentGroup != null) {
|
||||
if (currentGroup.level == 0 && currentGroup.title.equals("Backup", ignoreCase = true)) {
|
||||
return true
|
||||
}
|
||||
currentGroup = currentGroup.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isGroupSearchable(group: PwGroupV3?, omitBackup: Boolean): Boolean {
|
||||
return if (!super.isGroupSearchable(group, omitBackup)) {
|
||||
false
|
||||
} else !(omitBackup && isBackup(group!!))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_ENCRYPTION_ROUNDS = 300
|
||||
|
||||
/**
|
||||
* Encrypt the master key a few times to make brute-force key-search harder
|
||||
* @throws IOException
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
private fun transformMasterKey(pKeySeed: ByteArray, pKey: ByteArray, rounds: Long): ByteArray {
|
||||
val key = FinalKeyFactory.createFinalKey()
|
||||
|
||||
return key.transformMasterKey(pKeySeed, pKey, rounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,697 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element;
|
||||
|
||||
import android.util.Log;
|
||||
import biz.source_code.base64Coder.Base64Coder;
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary;
|
||||
import com.kunzisoft.keepass.crypto.CryptoUtil;
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine;
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine;
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine;
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory;
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters;
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException;
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF;
|
||||
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm;
|
||||
|
||||
import org.w3c.dom.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
public class PwDatabaseV4 extends PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
private static final String TAG = PwDatabaseV4.class.getName();
|
||||
|
||||
private static final int DEFAULT_HISTORY_MAX_ITEMS = 10; // -1 unlimited
|
||||
private static final long DEFAULT_HISTORY_MAX_SIZE = 6 * 1024 * 1024; // -1 unlimited
|
||||
private static final String RECYCLEBIN_NAME = "RecycleBin";
|
||||
|
||||
private PwGroupV4 rootGroup;
|
||||
|
||||
private byte[] hmacKey;
|
||||
private UUID dataCipher = AesEngine.CIPHER_UUID;
|
||||
private CipherEngine dataEngine = new AesEngine();
|
||||
private PwCompressionAlgorithm compressionAlgorithm = PwCompressionAlgorithm.Gzip;
|
||||
private KdfParameters kdfParameters;
|
||||
private long numKeyEncRounds;
|
||||
private VariantDictionary publicCustomData = new VariantDictionary();
|
||||
|
||||
private String name = "KeePass DX database";
|
||||
private PwDate nameChanged = new PwDate();
|
||||
private PwDate settingsChanged = new PwDate();
|
||||
private String description = "";
|
||||
private PwDate descriptionChanged = new PwDate();
|
||||
private String defaultUserName = "";
|
||||
private PwDate defaultUserNameChanged = new PwDate();
|
||||
|
||||
private PwDate keyLastChanged = new PwDate();
|
||||
private long keyChangeRecDays = -1;
|
||||
private long keyChangeForceDays = 1;
|
||||
private boolean keyChangeForceOnce = false;
|
||||
|
||||
private long maintenanceHistoryDays = 365;
|
||||
private String color = "";
|
||||
private boolean recycleBinEnabled = true;
|
||||
private UUID recycleBinUUID = UUID_ZERO;
|
||||
private Date recycleBinChanged = new Date();
|
||||
private UUID entryTemplatesGroup = UUID_ZERO;
|
||||
private PwDate entryTemplatesGroupChanged = new PwDate();
|
||||
private int historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS;
|
||||
private long historyMaxSize = DEFAULT_HISTORY_MAX_SIZE;
|
||||
private UUID lastSelectedGroup = UUID_ZERO;
|
||||
private UUID lastTopVisibleGroup = UUID_ZERO;
|
||||
private MemoryProtectionConfig memoryProtection = new MemoryProtectionConfig();
|
||||
private List<PwDeletedObject> deletedObjects = new ArrayList<>();
|
||||
private List<PwIconCustom> customIcons = new ArrayList<>();
|
||||
private Map<String, String> customData = new HashMap<>();
|
||||
|
||||
private BinaryPool binPool = new BinaryPool();
|
||||
|
||||
public String localizedAppName = "KeePassDX"; // TODO resource
|
||||
|
||||
public PwDatabaseV4() {}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "KeePass 2";
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public byte[] getHmacKey() {
|
||||
return hmacKey;
|
||||
}
|
||||
|
||||
public UUID getDataCipher() {
|
||||
return dataCipher;
|
||||
}
|
||||
|
||||
public void setDataCipher(UUID dataCipher) {
|
||||
this.dataCipher = dataCipher;
|
||||
}
|
||||
|
||||
public void setDataEngine(CipherEngine dataEngine) {
|
||||
this.dataEngine = dataEngine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PwEncryptionAlgorithm> getAvailableEncryptionAlgorithms() {
|
||||
List<PwEncryptionAlgorithm> list = new ArrayList<>();
|
||||
list.add(PwEncryptionAlgorithm.AESRijndael);
|
||||
list.add(PwEncryptionAlgorithm.Twofish);
|
||||
list.add(PwEncryptionAlgorithm.ChaCha20);
|
||||
return list;
|
||||
}
|
||||
|
||||
public PwCompressionAlgorithm getCompressionAlgorithm() {
|
||||
return compressionAlgorithm;
|
||||
}
|
||||
|
||||
public void setCompressionAlgorithm(PwCompressionAlgorithm compressionAlgorithm) {
|
||||
this.compressionAlgorithm = compressionAlgorithm;
|
||||
}
|
||||
|
||||
public @Nullable KdfEngine getKdfEngine() {
|
||||
try {
|
||||
return KdfFactory.getEngineV4(kdfParameters);
|
||||
} catch (UnknownKDF unknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public KdfParameters getKdfParameters() {
|
||||
return kdfParameters;
|
||||
}
|
||||
|
||||
public void setKdfParameters(KdfParameters kdfParameters) {
|
||||
this.kdfParameters = kdfParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNumberKeyEncryptionRounds() {
|
||||
if (getKdfEngine() != null && getKdfParameters() != null)
|
||||
numKeyEncRounds = getKdfEngine().getKeyRounds(getKdfParameters());
|
||||
return numKeyEncRounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNumberKeyEncryptionRounds(long rounds) throws NumberFormatException {
|
||||
if (getKdfEngine() != null && getKdfParameters() != null)
|
||||
getKdfEngine().setKeyRounds(getKdfParameters(), rounds);
|
||||
numKeyEncRounds = rounds;
|
||||
}
|
||||
|
||||
public long getMemoryUsage() {
|
||||
if (getKdfEngine() != null && getKdfParameters() != null) {
|
||||
return getKdfEngine().getMemoryUsage(getKdfParameters());
|
||||
}
|
||||
return KdfEngine.UNKNOW_VALUE;
|
||||
}
|
||||
|
||||
public void setMemoryUsage(long memory) {
|
||||
if (getKdfEngine() != null && getKdfParameters() != null)
|
||||
getKdfEngine().setMemoryUsage(getKdfParameters(), memory);
|
||||
}
|
||||
|
||||
public int getParallelism() {
|
||||
if (getKdfEngine() != null && getKdfParameters() != null) {
|
||||
return getKdfEngine().getParallelism(getKdfParameters());
|
||||
}
|
||||
return KdfEngine.UNKNOW_VALUE;
|
||||
}
|
||||
|
||||
public void setParallelism(int parallelism) {
|
||||
if (getKdfEngine() != null && getKdfParameters() != null)
|
||||
getKdfEngine().setParallelism(getKdfParameters(), parallelism);
|
||||
}
|
||||
|
||||
public PwDate getNameChanged() {
|
||||
return nameChanged;
|
||||
}
|
||||
|
||||
public void setNameChanged(PwDate nameChanged) {
|
||||
this.nameChanged = nameChanged;
|
||||
}
|
||||
|
||||
public PwDate getSettingsChanged() {
|
||||
return settingsChanged;
|
||||
}
|
||||
|
||||
public void setSettingsChanged(PwDate settingsChanged) {
|
||||
// TODO change setting date
|
||||
this.settingsChanged = settingsChanged;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public PwDate getDescriptionChanged() {
|
||||
return descriptionChanged;
|
||||
}
|
||||
|
||||
public void setDescriptionChanged(PwDate descriptionChanged) {
|
||||
this.descriptionChanged = descriptionChanged;
|
||||
}
|
||||
|
||||
public String getDefaultUserName() {
|
||||
return defaultUserName;
|
||||
}
|
||||
|
||||
public void setDefaultUserName(String defaultUserName) {
|
||||
this.defaultUserName = defaultUserName;
|
||||
}
|
||||
|
||||
public PwDate getDefaultUserNameChanged() {
|
||||
return defaultUserNameChanged;
|
||||
}
|
||||
|
||||
public void setDefaultUserNameChanged(PwDate defaultUserNameChanged) {
|
||||
this.defaultUserNameChanged = defaultUserNameChanged;
|
||||
}
|
||||
|
||||
public PwDate getKeyLastChanged() {
|
||||
return keyLastChanged;
|
||||
}
|
||||
|
||||
public void setKeyLastChanged(PwDate keyLastChanged) {
|
||||
// TODO date
|
||||
this.keyLastChanged = keyLastChanged;
|
||||
}
|
||||
|
||||
public long getKeyChangeRecDays() {
|
||||
return keyChangeRecDays;
|
||||
}
|
||||
|
||||
public void setKeyChangeRecDays(long keyChangeRecDays) {
|
||||
this.keyChangeRecDays = keyChangeRecDays;
|
||||
}
|
||||
|
||||
public long getKeyChangeForceDays() {
|
||||
return keyChangeForceDays;
|
||||
}
|
||||
|
||||
public void setKeyChangeForceDays(long keyChangeForceDays) {
|
||||
this.keyChangeForceDays = keyChangeForceDays;
|
||||
}
|
||||
|
||||
public boolean isKeyChangeForceOnce() {
|
||||
return keyChangeForceOnce;
|
||||
}
|
||||
|
||||
public void setKeyChangeForceOnce(boolean keyChangeForceOnce) {
|
||||
this.keyChangeForceOnce = keyChangeForceOnce;
|
||||
}
|
||||
|
||||
public long getMaintenanceHistoryDays() {
|
||||
return maintenanceHistoryDays;
|
||||
}
|
||||
|
||||
public void setMaintenanceHistoryDays(long maintenanceHistoryDays) {
|
||||
this.maintenanceHistoryDays = maintenanceHistoryDays;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public UUID getEntryTemplatesGroup() {
|
||||
return entryTemplatesGroup;
|
||||
}
|
||||
|
||||
public void setEntryTemplatesGroup(UUID entryTemplatesGroup) {
|
||||
this.entryTemplatesGroup = entryTemplatesGroup;
|
||||
}
|
||||
|
||||
public PwDate getEntryTemplatesGroupChanged() {
|
||||
return entryTemplatesGroupChanged;
|
||||
}
|
||||
|
||||
public void setEntryTemplatesGroupChanged(PwDate entryTemplatesGroupChanged) {
|
||||
this.entryTemplatesGroupChanged = entryTemplatesGroupChanged;
|
||||
}
|
||||
|
||||
public int getHistoryMaxItems() {
|
||||
return historyMaxItems;
|
||||
}
|
||||
|
||||
public void setHistoryMaxItems(int historyMaxItems) {
|
||||
this.historyMaxItems = historyMaxItems;
|
||||
}
|
||||
|
||||
public long getHistoryMaxSize() {
|
||||
return historyMaxSize;
|
||||
}
|
||||
|
||||
public void setHistoryMaxSize(long historyMaxSize) {
|
||||
this.historyMaxSize = historyMaxSize;
|
||||
}
|
||||
|
||||
public UUID getLastSelectedGroup() {
|
||||
return lastSelectedGroup;
|
||||
}
|
||||
|
||||
public void setLastSelectedGroup(UUID lastSelectedGroup) {
|
||||
this.lastSelectedGroup = lastSelectedGroup;
|
||||
}
|
||||
|
||||
public UUID getLastTopVisibleGroup() {
|
||||
return lastTopVisibleGroup;
|
||||
}
|
||||
|
||||
public void setLastTopVisibleGroup(UUID lastTopVisibleGroup) {
|
||||
this.lastTopVisibleGroup = lastTopVisibleGroup;
|
||||
}
|
||||
|
||||
public MemoryProtectionConfig getMemoryProtection() {
|
||||
return memoryProtection;
|
||||
}
|
||||
|
||||
public void setMemoryProtection(MemoryProtectionConfig memoryProtection) {
|
||||
this.memoryProtection = memoryProtection;
|
||||
}
|
||||
|
||||
public List<PwIconCustom> getCustomIcons() {
|
||||
return customIcons;
|
||||
}
|
||||
|
||||
public void addCustomIcon(PwIconCustom customIcon) {
|
||||
this.customIcons.add(customIcon);
|
||||
}
|
||||
|
||||
public Map<String, String> getCustomData() {
|
||||
return customData;
|
||||
}
|
||||
|
||||
public void putCustomData(String label, String value) {
|
||||
this.customData.put(label, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getMasterKey(@Nullable String key, @Nullable InputStream keyInputStream)
|
||||
throws InvalidKeyFileException, IOException {
|
||||
|
||||
byte[] fKey = new byte[]{};
|
||||
|
||||
if (key != null && keyInputStream != null) {
|
||||
return getCompositeKey(key, keyInputStream);
|
||||
} else if (key != null) { // key.length() >= 0
|
||||
fKey = getPasswordKey(key);
|
||||
} else if (keyInputStream != null) { // key == null
|
||||
fKey = getFileKey(keyInputStream);
|
||||
}
|
||||
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("No SHA-256 implementation");
|
||||
}
|
||||
|
||||
return md.digest(fKey);
|
||||
}
|
||||
|
||||
public void makeFinalKey(byte[] masterSeed) throws IOException {
|
||||
|
||||
KdfEngine kdfEngine = KdfFactory.getEngineV4(kdfParameters);
|
||||
|
||||
byte[] transformedMasterKey = kdfEngine.transform(masterKey, kdfParameters);
|
||||
if (transformedMasterKey.length != 32) {
|
||||
transformedMasterKey = CryptoUtil.hashSha256(transformedMasterKey);
|
||||
}
|
||||
|
||||
byte[] cmpKey = new byte[65];
|
||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32);
|
||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32);
|
||||
finalKey = CryptoUtil.resizeKey(cmpKey, 0, 64, dataEngine.keyLength());
|
||||
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-512");
|
||||
cmpKey[64] = 1;
|
||||
hmacKey = md.digest(cmpKey);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("No SHA-512 implementation");
|
||||
} finally {
|
||||
Arrays.fill(cmpKey, (byte)0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPasswordEncoding() {
|
||||
return "UTF-8";
|
||||
}
|
||||
|
||||
private static final String RootElementName = "KeyFile";
|
||||
//private static final String MetaElementName = "Meta";
|
||||
//private static final String VersionElementName = "Version";
|
||||
private static final String KeyElementName = "Key";
|
||||
private static final String KeyDataElementName = "Data";
|
||||
|
||||
@Override
|
||||
protected byte[] loadXmlKeyFile(InputStream keyInputStream) {
|
||||
try {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
Document doc = db.parse(keyInputStream);
|
||||
|
||||
Element el = doc.getDocumentElement();
|
||||
if (el == null || ! el.getNodeName().equalsIgnoreCase(RootElementName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
NodeList children = el.getChildNodes();
|
||||
if (children.getLength() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for ( int i = 0; i < children.getLength(); i++ ) {
|
||||
Node child = children.item(i);
|
||||
|
||||
if ( child.getNodeName().equalsIgnoreCase(KeyElementName) ) {
|
||||
NodeList keyChildren = child.getChildNodes();
|
||||
for ( int j = 0; j < keyChildren.getLength(); j++ ) {
|
||||
Node keyChild = keyChildren.item(j);
|
||||
if ( keyChild.getNodeName().equalsIgnoreCase(KeyDataElementName) ) {
|
||||
NodeList children2 = keyChild.getChildNodes();
|
||||
for ( int k = 0; k < children2.getLength(); k++) {
|
||||
Node text = children2.item(k);
|
||||
if (text.getNodeType() == Node.TEXT_NODE) {
|
||||
Text txt = (Text) text;
|
||||
return Base64Coder.decode(txt.getNodeValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwNodeIdUUID newGroupId() {
|
||||
PwNodeIdUUID newId;
|
||||
do {
|
||||
newId = new PwNodeIdUUID();
|
||||
} while (isGroupIdUsed(newId));
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwNodeIdUUID newEntryId() {
|
||||
PwNodeIdUUID newId;
|
||||
do {
|
||||
newId = new PwNodeIdUUID();
|
||||
} while (isEntryIdUsed(newId));
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwGroupV4 createGroup() {
|
||||
return new PwGroupV4();
|
||||
}
|
||||
|
||||
public void setRootGroup(PwGroupV4 rootGroup) {
|
||||
this.rootGroup = rootGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PwGroupV4 getRootGroup() {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public PwEntryV4 createEntry() {
|
||||
return new PwEntryV4();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBackup(PwGroupV4 group) {
|
||||
if (!recycleBinEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return group.isContainedIn(getRecycleBin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the recycle bin tree exists, if enabled and create it
|
||||
* if it doesn't exist
|
||||
*/
|
||||
private void ensureRecycleBin() {
|
||||
if (getRecycleBin() == null) {
|
||||
// Create recycle bin
|
||||
|
||||
PwGroupV4 recycleBin = createGroup();
|
||||
recycleBin.setTitle(RECYCLEBIN_NAME);
|
||||
recycleBin.setIcon(iconFactory.getTrashIcon());
|
||||
recycleBin.setEnableAutoType(false);
|
||||
recycleBin.setEnableSearching(false);
|
||||
recycleBin.setExpanded(false);
|
||||
addGroupTo(recycleBin, rootGroup);
|
||||
|
||||
recycleBinUUID = recycleBin.getId();
|
||||
}
|
||||
}
|
||||
|
||||
public UUID getRecycleBinUUID() {
|
||||
return recycleBinUUID;
|
||||
}
|
||||
|
||||
public void setRecycleBinUUID(UUID recycleBinUUID) {
|
||||
this.recycleBinUUID = recycleBinUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if RecycleBin is enable or not
|
||||
* @return true if RecycleBin enable, false if is not available or not enable
|
||||
*/
|
||||
public boolean isRecycleBinEnabled() {
|
||||
return recycleBinEnabled;
|
||||
}
|
||||
|
||||
public void setRecycleBinEnabled(boolean recycleBinEnabled) {
|
||||
this.recycleBinEnabled = recycleBinEnabled;
|
||||
}
|
||||
|
||||
public Date getRecycleBinChanged() {
|
||||
return recycleBinChanged;
|
||||
}
|
||||
|
||||
public void setRecycleBinChanged(Date recycleBinChanged) {
|
||||
// TODO recyclebin Date
|
||||
this.recycleBinChanged = recycleBinChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define if a Group must be delete or recycle
|
||||
* @param group Group to remove
|
||||
* @return true if group can be recycle, false elsewhere
|
||||
*/
|
||||
public boolean canRecycle(PwGroupV4 group) {
|
||||
if (!recycleBinEnabled) {
|
||||
return false;
|
||||
}
|
||||
PwGroupV4 recycle = getRecycleBin();
|
||||
return (recycle == null) || (!group.isContainedIn(recycle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define if an Entry must be delete or recycle
|
||||
* @param entry Entry to remove
|
||||
* @return true if entry can be recycle, false elsewhere
|
||||
*/
|
||||
public boolean canRecycle(PwEntryV4 entry) {
|
||||
if (!recycleBinEnabled) {
|
||||
return false;
|
||||
}
|
||||
PwGroupV4 parent = entry.getParent();
|
||||
return (parent != null) && canRecycle(parent);
|
||||
}
|
||||
|
||||
public void recycle(PwGroupV4 group) {
|
||||
ensureRecycleBin();
|
||||
|
||||
removeGroupFrom(group, group.getParent());
|
||||
|
||||
addGroupTo(group, getRecycleBin());
|
||||
|
||||
// TODO ? group.afterChangeParent();
|
||||
}
|
||||
|
||||
public void recycle(PwEntryV4 entry) {
|
||||
ensureRecycleBin();
|
||||
|
||||
removeEntryFrom(entry, entry.getParent());
|
||||
|
||||
addEntryTo(entry, getRecycleBin());
|
||||
|
||||
entry.afterChangeParent();
|
||||
}
|
||||
|
||||
public void undoRecycle(PwGroupV4 group, PwGroupV4 origParent) {
|
||||
|
||||
removeGroupFrom(group, getRecycleBin());
|
||||
|
||||
addGroupTo(group, origParent);
|
||||
}
|
||||
|
||||
public void undoRecycle(PwEntryV4 entry, PwGroupV4 origParent) {
|
||||
|
||||
removeEntryFrom(entry, getRecycleBin());
|
||||
|
||||
addEntryTo(entry, origParent);
|
||||
}
|
||||
|
||||
public List<PwDeletedObject> getDeletedObjects() {
|
||||
return deletedObjects;
|
||||
}
|
||||
|
||||
public void addDeletedObject(PwDeletedObject deletedObject) {
|
||||
this.deletedObjects.add(deletedObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeEntryFrom(PwEntryV4 entryToRemove, PwGroupV4 parent) {
|
||||
super.removeEntryFrom(entryToRemove, parent);
|
||||
deletedObjects.add(new PwDeletedObject(entryToRemove.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undoDeleteEntryFrom(PwEntryV4 entry, PwGroupV4 origParent) {
|
||||
super.undoDeleteEntryFrom(entry, origParent);
|
||||
deletedObjects.remove(new PwDeletedObject(entry.getId()));
|
||||
}
|
||||
|
||||
public PwGroupV4 getRecycleBin() { // TODO delete recycle bin preference
|
||||
if (recycleBinUUID == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PwNodeId recycleId = new PwNodeIdUUID(recycleBinUUID);
|
||||
return groupIndexes.get(recycleId);
|
||||
}
|
||||
|
||||
public VariantDictionary getPublicCustomData() {
|
||||
return publicCustomData;
|
||||
}
|
||||
|
||||
public boolean containsPublicCustomData() {
|
||||
return publicCustomData.size() > 0;
|
||||
}
|
||||
|
||||
public void setPublicCustomData(VariantDictionary publicCustomData) {
|
||||
this.publicCustomData = publicCustomData;
|
||||
}
|
||||
|
||||
public BinaryPool getBinPool() {
|
||||
return binPool;
|
||||
}
|
||||
|
||||
public void setBinPool(BinaryPool binPool) {
|
||||
this.binPool = binPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroupSearchable(PwGroupV4 group, boolean omitBackup) {
|
||||
if (!super.isGroupSearchable(group, omitBackup)) {
|
||||
return false;
|
||||
}
|
||||
return group.isSearchingEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validatePasswordEncoding(String key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
binPool.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.util.Log
|
||||
import biz.source_code.base64Coder.Base64Coder
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.Text
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
|
||||
class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
|
||||
|
||||
var hmacKey: ByteArray? = null
|
||||
private set
|
||||
var dataCipher = AesEngine.CIPHER_UUID
|
||||
private var dataEngine: CipherEngine = AesEngine()
|
||||
var compressionAlgorithm = PwCompressionAlgorithm.Gzip
|
||||
var kdfParameters: KdfParameters? = null
|
||||
private var numKeyEncRounds: Long = 0
|
||||
var publicCustomData = VariantDictionary()
|
||||
|
||||
var name = "KeePass DX database"
|
||||
var nameChanged = PwDate()
|
||||
// TODO change setting date
|
||||
var settingsChanged = PwDate()
|
||||
var description = ""
|
||||
var descriptionChanged = PwDate()
|
||||
var defaultUserName = ""
|
||||
var defaultUserNameChanged = PwDate()
|
||||
|
||||
// TODO date
|
||||
var keyLastChanged = PwDate()
|
||||
var keyChangeRecDays: Long = -1
|
||||
var keyChangeForceDays: Long = 1
|
||||
var isKeyChangeForceOnce = false
|
||||
|
||||
var maintenanceHistoryDays: Long = 365
|
||||
var color = ""
|
||||
/**
|
||||
* Determine if RecycleBin is enable or not
|
||||
* @return true if RecycleBin enable, false if is not available or not enable
|
||||
*/
|
||||
var isRecycleBinEnabled = true
|
||||
var recycleBinUUID: UUID = UUID_ZERO
|
||||
var recycleBinChanged = Date()
|
||||
var entryTemplatesGroup = UUID_ZERO
|
||||
var entryTemplatesGroupChanged = PwDate()
|
||||
var historyMaxItems = DEFAULT_HISTORY_MAX_ITEMS
|
||||
var historyMaxSize = DEFAULT_HISTORY_MAX_SIZE
|
||||
var lastSelectedGroupUUID = UUID_ZERO
|
||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||
var memoryProtection = MemoryProtectionConfig()
|
||||
val deletedObjects = ArrayList<PwDeletedObject>()
|
||||
val customIcons = ArrayList<PwIconCustom>()
|
||||
val customData = HashMap<String, String>()
|
||||
|
||||
var binPool = BinaryPool()
|
||||
|
||||
var localizedAppName = "KeePassDX" // TODO resource
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(databaseName: String) {
|
||||
val groupV4 = createGroup().apply {
|
||||
title = databaseName
|
||||
icon = iconFactory.folderIcon
|
||||
}
|
||||
rootGroup = groupV4
|
||||
addGroupIndex(groupV4)
|
||||
}
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 2"
|
||||
|
||||
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
|
||||
get() {
|
||||
val list = ArrayList<PwEncryptionAlgorithm>()
|
||||
list.add(PwEncryptionAlgorithm.AESRijndael)
|
||||
list.add(PwEncryptionAlgorithm.Twofish)
|
||||
list.add(PwEncryptionAlgorithm.ChaCha20)
|
||||
return list
|
||||
}
|
||||
|
||||
val kdfEngine: KdfEngine?
|
||||
get() {
|
||||
try {
|
||||
return KdfFactory.getEngineV4(kdfParameters)
|
||||
} catch (unknownKDF: UnknownKDF) {
|
||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override var numberKeyEncryptionRounds: Long
|
||||
get() {
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
numKeyEncRounds = kdfEngine!!.getKeyRounds(kdfParameters)
|
||||
return numKeyEncRounds
|
||||
}
|
||||
@Throws(NumberFormatException::class)
|
||||
set(rounds) {
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setKeyRounds(kdfParameters, rounds)
|
||||
numKeyEncRounds = rounds
|
||||
}
|
||||
|
||||
var memoryUsage: Long
|
||||
get() = if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine!!.getMemoryUsage(kdfParameters)
|
||||
} else KdfEngine.UNKNOW_VALUE.toLong()
|
||||
set(memory) {
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setMemoryUsage(kdfParameters, memory)
|
||||
}
|
||||
|
||||
var parallelism: Int
|
||||
get() = if (kdfEngine != null && kdfParameters != null) {
|
||||
kdfEngine!!.getParallelism(kdfParameters)
|
||||
} else KdfEngine.UNKNOW_VALUE
|
||||
set(parallelism) {
|
||||
if (kdfEngine != null && kdfParameters != null)
|
||||
kdfEngine!!.setParallelism(kdfParameters, parallelism)
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "UTF-8"
|
||||
|
||||
fun getGroupByUUID(groupUUID: UUID): PwGroupV4? {
|
||||
if (groupUUID == UUID_ZERO)
|
||||
return null
|
||||
return getGroupById(PwNodeIdUUID(groupUUID))
|
||||
}
|
||||
|
||||
// Retrieve recycle bin in index
|
||||
val recycleBin: PwGroupV4?
|
||||
get() = getGroupByUUID(recycleBinUUID)
|
||||
|
||||
val lastSelectedGroup: PwGroupV4?
|
||||
get() = getGroupByUUID(lastSelectedGroupUUID)
|
||||
|
||||
val lastTopVisibleGroup: PwGroupV4?
|
||||
get() = getGroupByUUID(lastTopVisibleGroupUUID)
|
||||
|
||||
fun setDataEngine(dataEngine: CipherEngine) {
|
||||
this.dataEngine = dataEngine
|
||||
}
|
||||
|
||||
fun getCustomIcons(): List<PwIconCustom> {
|
||||
return customIcons
|
||||
}
|
||||
|
||||
fun addCustomIcon(customIcon: PwIconCustom) {
|
||||
this.customIcons.add(customIcon)
|
||||
}
|
||||
|
||||
fun getCustomData(): Map<String, String> {
|
||||
return customData
|
||||
}
|
||||
|
||||
fun putCustomData(label: String, value: String) {
|
||||
this.customData[label] = value
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
|
||||
|
||||
var fKey = byteArrayOf()
|
||||
|
||||
if (key != null && keyInputStream != null) {
|
||||
return getCompositeKey(key, keyInputStream)
|
||||
} else if (key != null) { // key.length() >= 0
|
||||
fKey = getPasswordKey(key)
|
||||
} else if (keyInputStream != null) { // key == null
|
||||
fKey = getFileKey(keyInputStream)
|
||||
}
|
||||
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("No SHA-256 implementation")
|
||||
}
|
||||
|
||||
return md.digest(fKey)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
val kdfEngine = KdfFactory.getEngineV4(kdfParameters)
|
||||
|
||||
var transformedMasterKey = kdfEngine.transform(masterKey, kdfParameters)
|
||||
if (transformedMasterKey.size != 32) {
|
||||
transformedMasterKey = CryptoUtil.hashSha256(transformedMasterKey)
|
||||
}
|
||||
|
||||
val cmpKey = ByteArray(65)
|
||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||
finalKey = CryptoUtil.resizeKey(cmpKey, 0, 64, dataEngine.keyLength())
|
||||
|
||||
val md: MessageDigest
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-512")
|
||||
cmpKey[64] = 1
|
||||
hmacKey = md.digest(cmpKey)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("No SHA-512 implementation")
|
||||
} finally {
|
||||
Arrays.fill(cmpKey, 0.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
try {
|
||||
val dbf = DocumentBuilderFactory.newInstance()
|
||||
val db = dbf.newDocumentBuilder()
|
||||
val doc = db.parse(keyInputStream)
|
||||
|
||||
val el = doc.documentElement
|
||||
if (el == null || !el.nodeName.equals(RootElementName, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val children = el.childNodes
|
||||
if (children.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (i in 0 until children.length) {
|
||||
val child = children.item(i)
|
||||
|
||||
if (child.nodeName.equals(KeyElementName, ignoreCase = true)) {
|
||||
val keyChildren = child.childNodes
|
||||
for (j in 0 until keyChildren.length) {
|
||||
val keyChild = keyChildren.item(j)
|
||||
if (keyChild.nodeName.equals(KeyDataElementName, ignoreCase = true)) {
|
||||
val children2 = keyChild.childNodes
|
||||
for (k in 0 until children2.length) {
|
||||
val text = children2.item(k)
|
||||
if (text.nodeType == Node.TEXT_NODE) {
|
||||
val txt = text as Text
|
||||
return Base64Coder.decode(txt.nodeValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun newGroupId(): PwNodeIdUUID {
|
||||
var newId: PwNodeIdUUID
|
||||
do {
|
||||
newId = PwNodeIdUUID()
|
||||
} while (isGroupIdUsed(newId))
|
||||
|
||||
return newId
|
||||
}
|
||||
|
||||
override fun newEntryId(): PwNodeIdUUID {
|
||||
var newId: PwNodeIdUUID
|
||||
do {
|
||||
newId = PwNodeIdUUID()
|
||||
} while (isEntryIdUsed(newId))
|
||||
|
||||
return newId
|
||||
}
|
||||
|
||||
override fun createGroup(): PwGroupV4 {
|
||||
return PwGroupV4()
|
||||
}
|
||||
|
||||
override fun createEntry(): PwEntryV4 {
|
||||
return PwEntryV4()
|
||||
}
|
||||
|
||||
override fun isBackup(group: PwGroupV4): Boolean {
|
||||
if (recycleBin == null)
|
||||
return false
|
||||
return if (!isRecycleBinEnabled) {
|
||||
false
|
||||
} else group.isContainedIn(recycleBin!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the recycle bin tree exists, if enabled and create it
|
||||
* if it doesn't exist
|
||||
*/
|
||||
private fun ensureRecycleBin(resources: Resources) {
|
||||
if (recycleBin == null) {
|
||||
// Create recycle bin
|
||||
val recycleBinGroup = createGroup().apply {
|
||||
title = resources.getString(R.string.recycle_bin)
|
||||
icon = iconFactory.trashIcon
|
||||
enableAutoType = false
|
||||
enableSearching = false
|
||||
isExpanded = false
|
||||
}
|
||||
addGroupTo(recycleBinGroup, rootGroup)
|
||||
recycleBinUUID = recycleBinGroup.id
|
||||
recycleBinGroup.lastModificationTime.date?.let {
|
||||
recycleBinChanged = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define if a Node must be delete or recycle when remove action is called
|
||||
* @param node Node to remove
|
||||
* @return true if node can be recycle, false elsewhere
|
||||
*/
|
||||
fun canRecycle(node: PwNode<*, PwGroupV4, PwEntryV4>): Boolean {
|
||||
if (!isRecycleBinEnabled)
|
||||
return false
|
||||
if (recycleBin == null)
|
||||
return true
|
||||
if (!node.isContainedIn(recycleBin!!))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun recycle(group: PwGroupV4, resources: Resources) {
|
||||
ensureRecycleBin(resources)
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, recycleBin)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: PwEntryV4, resources: Resources) {
|
||||
ensureRecycleBin(resources)
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, recycleBin)
|
||||
entry.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun undoRecycle(group: PwGroupV4, origParent: PwGroupV4) {
|
||||
removeGroupFrom(group, recycleBin)
|
||||
addGroupTo(group, origParent)
|
||||
}
|
||||
|
||||
fun undoRecycle(entry: PwEntryV4, origParent: PwGroupV4) {
|
||||
removeEntryFrom(entry, recycleBin)
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun getDeletedObjects(): List<PwDeletedObject> {
|
||||
return deletedObjects
|
||||
}
|
||||
|
||||
fun addDeletedObject(deletedObject: PwDeletedObject) {
|
||||
this.deletedObjects.add(deletedObject)
|
||||
}
|
||||
|
||||
override fun removeEntryFrom(entryToRemove: PwEntryV4, parent: PwGroupV4?) {
|
||||
super.removeEntryFrom(entryToRemove, parent)
|
||||
deletedObjects.add(PwDeletedObject(entryToRemove.id))
|
||||
}
|
||||
|
||||
override fun undoDeleteEntryFrom(entry: PwEntryV4, origParent: PwGroupV4?) {
|
||||
super.undoDeleteEntryFrom(entry, origParent)
|
||||
deletedObjects.remove(PwDeletedObject(entry.id))
|
||||
}
|
||||
|
||||
fun containsPublicCustomData(): Boolean {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
override fun isGroupSearchable(group: PwGroupV4?, omitBackup: Boolean): Boolean {
|
||||
return if (!super.isGroupSearchable(group, omitBackup)) {
|
||||
false
|
||||
} else group!!.isSearchingEnabled
|
||||
}
|
||||
|
||||
override fun validatePasswordEncoding(key: String?): Boolean {
|
||||
if (key == null)
|
||||
return true
|
||||
return super.validatePasswordEncoding(key)
|
||||
}
|
||||
|
||||
override fun clearCache() {
|
||||
super.clearCache()
|
||||
binPool.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = PwDatabaseV4::class.java.name
|
||||
|
||||
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
|
||||
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
|
||||
|
||||
private const val RootElementName = "KeyFile"
|
||||
//private const val MetaElementName = "Meta";
|
||||
//private const val VersionElementName = "Version";
|
||||
private const val KeyElementName = "Key"
|
||||
private const val KeyDataElementName = "Data"
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class PwDatabaseV4XML {
|
||||
|
||||
public static final String ElemDocNode = "KeePassFile";
|
||||
public static final String ElemMeta = "Meta";
|
||||
public static final String ElemRoot = "Root";
|
||||
public static final String ElemGroup = "Group";
|
||||
public static final String ElemEntry = "Entry";
|
||||
|
||||
public static final String ElemGenerator = "Generator";
|
||||
public static final String ElemHeaderHash = "HeaderHash";
|
||||
public static final String ElemSettingsChanged = "SettingsChanged";
|
||||
public static final String ElemDbName = "DatabaseName";
|
||||
public static final String ElemDbNameChanged = "DatabaseNameChanged";
|
||||
public static final String ElemDbDesc = "DatabaseDescription";
|
||||
public static final String ElemDbDescChanged = "DatabaseDescriptionChanged";
|
||||
public static final String ElemDbDefaultUser = "DefaultUserName";
|
||||
public static final String ElemDbDefaultUserChanged = "DefaultUserNameChanged";
|
||||
public static final String ElemDbMntncHistoryDays = "MaintenanceHistoryDays";
|
||||
public static final String ElemDbColor = "Color";
|
||||
public static final String ElemDbKeyChanged = "MasterKeyChanged";
|
||||
public static final String ElemDbKeyChangeRec = "MasterKeyChangeRec";
|
||||
public static final String ElemDbKeyChangeForce = "MasterKeyChangeForce";
|
||||
public static final String ElemDbKeyChangeForceOnce = "MasterKeyChangeForceOnce";
|
||||
public static final String ElemRecycleBinEnabled = "RecycleBinEnabled";
|
||||
public static final String ElemRecycleBinUuid = "RecycleBinUUID";
|
||||
public static final String ElemRecycleBinChanged = "RecycleBinChanged";
|
||||
public static final String ElemEntryTemplatesGroup = "EntryTemplatesGroup";
|
||||
public static final String ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged";
|
||||
public static final String ElemHistoryMaxItems = "HistoryMaxItems";
|
||||
public static final String ElemHistoryMaxSize = "HistoryMaxSize";
|
||||
public static final String ElemLastSelectedGroup = "LastSelectedGroup";
|
||||
public static final String ElemLastTopVisibleGroup = "LastTopVisibleGroup";
|
||||
|
||||
public static final String ElemMemoryProt = "MemoryProtection";
|
||||
public static final String ElemProtTitle = "ProtectTitle";
|
||||
public static final String ElemProtUserName = "ProtectUserName";
|
||||
public static final String ElemProtPassword = "ProtectPassword";
|
||||
public static final String ElemProtURL = "ProtectURL";
|
||||
public static final String ElemProtNotes = "ProtectNotes";
|
||||
public static final String ElemProtAutoHide = "AutoEnableVisualHiding";
|
||||
|
||||
public static final String ElemCustomIcons = "CustomIcons";
|
||||
public static final String ElemCustomIconItem = "Icon";
|
||||
public static final String ElemCustomIconItemID = "UUID";
|
||||
public static final String ElemCustomIconItemData = "Data";
|
||||
|
||||
public static final String ElemAutoType = "AutoType";
|
||||
public static final String ElemHistory = "History";
|
||||
|
||||
public static final String ElemName = "Name";
|
||||
public static final String ElemNotes = "Notes";
|
||||
public static final String ElemUuid = "UUID";
|
||||
public static final String ElemIcon = "IconID";
|
||||
public static final String ElemCustomIconID = "CustomIconUUID";
|
||||
public static final String ElemFgColor = "ForegroundColor";
|
||||
public static final String ElemBgColor = "BackgroundColor";
|
||||
public static final String ElemOverrideUrl = "OverrideURL";
|
||||
public static final String ElemTimes = "Times";
|
||||
public static final String ElemTags = "Tags";
|
||||
|
||||
public static final String ElemCreationTime = "CreationTime";
|
||||
public static final String ElemLastModTime = "LastModificationTime";
|
||||
public static final String ElemLastAccessTime = "LastAccessTime";
|
||||
public static final String ElemExpiryTime = "ExpiryTime";
|
||||
public static final String ElemExpires = "Expires";
|
||||
public static final String ElemUsageCount = "UsageCount";
|
||||
public static final String ElemLocationChanged = "LocationChanged";
|
||||
|
||||
public static final String ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence";
|
||||
public static final String ElemEnableAutoType = "EnableAutoType";
|
||||
public static final String ElemEnableSearching = "EnableSearching";
|
||||
|
||||
public static final String ElemString = "String";
|
||||
public static final String ElemBinary = "Binary";
|
||||
public static final String ElemKey = "Key";
|
||||
public static final String ElemValue = "Value";
|
||||
|
||||
public static final String ElemAutoTypeEnabled = "Enabled";
|
||||
public static final String ElemAutoTypeObfuscation = "DataTransferObfuscation";
|
||||
public static final String ElemAutoTypeDefaultSeq = "DefaultSequence";
|
||||
public static final String ElemAutoTypeItem = "Association";
|
||||
public static final String ElemWindow = "Window";
|
||||
public static final String ElemKeystrokeSequence = "KeystrokeSequence";
|
||||
|
||||
public static final String ElemBinaries = "Binaries";
|
||||
|
||||
public static final String AttrId = "ID";
|
||||
public static final String AttrRef = "Ref";
|
||||
public static final String AttrProtected = "Protected";
|
||||
public static final String AttrCompressed = "Compressed";
|
||||
|
||||
public static final String ElemIsExpanded = "IsExpanded";
|
||||
public static final String ElemLastTopVisibleEntry = "LastTopVisibleEntry";
|
||||
|
||||
public static final String ElemDeletedObjects = "DeletedObjects";
|
||||
public static final String ElemDeletedObject = "DeletedObject";
|
||||
public static final String ElemDeletionTime = "DeletionTime";
|
||||
|
||||
public static final String ValFalse = "False";
|
||||
public static final String ValTrue = "True";
|
||||
|
||||
public static final String ElemCustomData = "CustomData";
|
||||
public static final String ElemStringDictExItem = "Item";
|
||||
|
||||
public static final ThreadLocal<SimpleDateFormat> dateFormatter =
|
||||
new ThreadLocal<SimpleDateFormat>() {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
SimpleDateFormat dateFormat;
|
||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return dateFormat;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object PwDatabaseV4XML {
|
||||
|
||||
const val ElemDocNode = "KeePassFile"
|
||||
const val ElemMeta = "Meta"
|
||||
const val ElemRoot = "Root"
|
||||
const val ElemGroup = "Group"
|
||||
const val ElemEntry = "Entry"
|
||||
|
||||
const val ElemGenerator = "Generator"
|
||||
const val ElemHeaderHash = "HeaderHash"
|
||||
const val ElemSettingsChanged = "SettingsChanged"
|
||||
const val ElemDbName = "DatabaseName"
|
||||
const val ElemDbNameChanged = "DatabaseNameChanged"
|
||||
const val ElemDbDesc = "DatabaseDescription"
|
||||
const val ElemDbDescChanged = "DatabaseDescriptionChanged"
|
||||
const val ElemDbDefaultUser = "DefaultUserName"
|
||||
const val ElemDbDefaultUserChanged = "DefaultUserNameChanged"
|
||||
const val ElemDbMntncHistoryDays = "MaintenanceHistoryDays"
|
||||
const val ElemDbColor = "Color"
|
||||
const val ElemDbKeyChanged = "MasterKeyChanged"
|
||||
const val ElemDbKeyChangeRec = "MasterKeyChangeRec"
|
||||
const val ElemDbKeyChangeForce = "MasterKeyChangeForce"
|
||||
const val ElemDbKeyChangeForceOnce = "MasterKeyChangeForceOnce"
|
||||
const val ElemRecycleBinEnabled = "RecycleBinEnabled"
|
||||
const val ElemRecycleBinUuid = "RecycleBinUUID"
|
||||
const val ElemRecycleBinChanged = "RecycleBinChanged"
|
||||
const val ElemEntryTemplatesGroup = "EntryTemplatesGroup"
|
||||
const val ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged"
|
||||
const val ElemHistoryMaxItems = "HistoryMaxItems"
|
||||
const val ElemHistoryMaxSize = "HistoryMaxSize"
|
||||
const val ElemLastSelectedGroup = "LastSelectedGroup"
|
||||
const val ElemLastTopVisibleGroup = "LastTopVisibleGroup"
|
||||
|
||||
const val ElemMemoryProt = "MemoryProtection"
|
||||
const val ElemProtTitle = "ProtectTitle"
|
||||
const val ElemProtUserName = "ProtectUserName"
|
||||
const val ElemProtPassword = "ProtectPassword"
|
||||
const val ElemProtURL = "ProtectURL"
|
||||
const val ElemProtNotes = "ProtectNotes"
|
||||
const val ElemProtAutoHide = "AutoEnableVisualHiding"
|
||||
|
||||
const val ElemCustomIcons = "CustomIcons"
|
||||
const val ElemCustomIconItem = "Icon"
|
||||
const val ElemCustomIconItemID = "UUID"
|
||||
const val ElemCustomIconItemData = "Data"
|
||||
|
||||
const val ElemAutoType = "AutoType"
|
||||
const val ElemHistory = "History"
|
||||
|
||||
const val ElemName = "Name"
|
||||
const val ElemNotes = "Notes"
|
||||
const val ElemUuid = "UUID"
|
||||
const val ElemIcon = "IconID"
|
||||
const val ElemCustomIconID = "CustomIconUUID"
|
||||
const val ElemFgColor = "ForegroundColor"
|
||||
const val ElemBgColor = "BackgroundColor"
|
||||
const val ElemOverrideUrl = "OverrideURL"
|
||||
const val ElemTimes = "Times"
|
||||
const val ElemTags = "Tags"
|
||||
|
||||
const val ElemCreationTime = "CreationTime"
|
||||
const val ElemLastModTime = "LastModificationTime"
|
||||
const val ElemLastAccessTime = "LastAccessTime"
|
||||
const val ElemExpiryTime = "ExpiryTime"
|
||||
const val ElemExpires = "Expires"
|
||||
const val ElemUsageCount = "UsageCount"
|
||||
const val ElemLocationChanged = "LocationChanged"
|
||||
|
||||
const val ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence"
|
||||
const val ElemEnableAutoType = "EnableAutoType"
|
||||
const val ElemEnableSearching = "EnableSearching"
|
||||
|
||||
const val ElemString = "String"
|
||||
const val ElemBinary = "Binary"
|
||||
const val ElemKey = "Key"
|
||||
const val ElemValue = "Value"
|
||||
|
||||
const val ElemAutoTypeEnabled = "Enabled"
|
||||
const val ElemAutoTypeObfuscation = "DataTransferObfuscation"
|
||||
const val ElemAutoTypeDefaultSeq = "DefaultSequence"
|
||||
const val ElemAutoTypeItem = "Association"
|
||||
const val ElemWindow = "Window"
|
||||
const val ElemKeystrokeSequence = "KeystrokeSequence"
|
||||
|
||||
const val ElemBinaries = "Binaries"
|
||||
|
||||
const val AttrId = "ID"
|
||||
const val AttrRef = "Ref"
|
||||
const val AttrProtected = "Protected"
|
||||
const val AttrCompressed = "Compressed"
|
||||
|
||||
const val ElemIsExpanded = "IsExpanded"
|
||||
const val ElemLastTopVisibleEntry = "LastTopVisibleEntry"
|
||||
|
||||
const val ElemDeletedObjects = "DeletedObjects"
|
||||
const val ElemDeletedObject = "DeletedObject"
|
||||
const val ElemDeletionTime = "DeletionTime"
|
||||
|
||||
const val ValFalse = "False"
|
||||
const val ValTrue = "True"
|
||||
|
||||
const val ElemCustomData = "CustomData"
|
||||
const val ElemStringDictExItem = "Item"
|
||||
|
||||
val dateFormatter: ThreadLocal<SimpleDateFormat> = object : ThreadLocal<SimpleDateFormat>() {
|
||||
override fun initialValue(): SimpleDateFormat {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return dateFormat
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.MemUtil
|
||||
import com.kunzisoft.keepass.utils.SprEngineV4
|
||||
import java.util.*
|
||||
|
||||
class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
|
||||
@@ -167,7 +166,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a reference key woth the SprEngineV4
|
||||
* Decode a reference key with the SprEngineV4
|
||||
* @param decodeRef
|
||||
* @param key
|
||||
* @return
|
||||
@@ -175,7 +174,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
|
||||
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
|
||||
val text = fields.getProtectedStringValue(key)
|
||||
return if (decodeRef) {
|
||||
if (mDatabase == null) text else SprEngineV4().compile(text, this, mDatabase)
|
||||
if (mDatabase == null) text else SprEngineV4().compile(text, this, mDatabase!!)
|
||||
} else text
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import com.kunzisoft.keepass.database.search.EntrySearchHandlerV4
|
||||
import com.kunzisoft.keepass.database.search.SearchParametersV4
|
||||
import com.kunzisoft.keepass.utils.StringUtil
|
||||
import java.util.*
|
||||
|
||||
class SprEngineV4 {
|
||||
|
||||
inner class TargetResult(var entry: PwEntryV4?, var wanted: Char)
|
||||
|
||||
private inner class SprContextV4 {
|
||||
|
||||
var databaseV4: PwDatabaseV4? = null
|
||||
var entry: PwEntryV4
|
||||
var refsCache: MutableMap<String, String> = HashMap()
|
||||
|
||||
internal constructor(db: PwDatabaseV4, entry: PwEntryV4) {
|
||||
this.databaseV4 = db
|
||||
this.entry = entry
|
||||
}
|
||||
|
||||
internal constructor(source: SprContextV4) {
|
||||
this.databaseV4 = source.databaseV4
|
||||
this.entry = source.entry
|
||||
this.refsCache = source.refsCache
|
||||
}
|
||||
}
|
||||
|
||||
fun compile(text: String, entry: PwEntryV4, database: PwDatabaseV4): String {
|
||||
return compileInternal(text, SprContextV4(database, entry), 0)
|
||||
}
|
||||
|
||||
private fun compileInternal(text: String?, sprContextV4: SprContextV4?, recursionLevel: Int): String {
|
||||
if (text == null) {
|
||||
return ""
|
||||
}
|
||||
if (sprContextV4 == null) {
|
||||
return ""
|
||||
}
|
||||
return if (recursionLevel >= MAX_RECURSION_DEPTH) {
|
||||
""
|
||||
} else fillRefPlaceholders(text, sprContextV4, recursionLevel)
|
||||
|
||||
}
|
||||
|
||||
private fun fillRefPlaceholders(textReference: String, contextV4: SprContextV4, recursionLevel: Int): String {
|
||||
var text = textReference
|
||||
|
||||
if (contextV4.databaseV4 == null) {
|
||||
return text
|
||||
}
|
||||
|
||||
var offset = 0
|
||||
for (i in 0..19) {
|
||||
text = fillRefsUsingCache(text, contextV4)
|
||||
|
||||
val start = StringUtil.indexOfIgnoreCase(text, STR_REF_START, offset, Locale.ENGLISH)
|
||||
if (start < 0) {
|
||||
break
|
||||
}
|
||||
val end = StringUtil.indexOfIgnoreCase(text, STR_REF_END, start + 1, Locale.ENGLISH)
|
||||
if (end <= start) {
|
||||
break
|
||||
}
|
||||
|
||||
val fullRef = text.substring(start, end - start + 1)
|
||||
val result = findRefTarget(fullRef, contextV4)
|
||||
|
||||
if (result != null) {
|
||||
val found = result.entry
|
||||
val wanted = result.wanted
|
||||
|
||||
var data: String? = null
|
||||
when (wanted) {
|
||||
'T' -> data = found?.title
|
||||
'U' -> data = found?.username
|
||||
'A' -> data = found?.url
|
||||
'P' -> data = found?.password
|
||||
'N' -> data = found?.notes
|
||||
'I' -> data = found?.nodeId.toString()
|
||||
}
|
||||
|
||||
if (data != null && found != null) {
|
||||
val subCtx = SprContextV4(contextV4)
|
||||
subCtx.entry = found
|
||||
|
||||
val innerContent = compileInternal(data, subCtx, recursionLevel + 1)
|
||||
addRefsToCache(fullRef, innerContent, contextV4)
|
||||
text = fillRefsUsingCache(text, contextV4)
|
||||
} else {
|
||||
offset = start + 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private fun findRefTarget(fullReference: String?, contextV4: SprContextV4): TargetResult? {
|
||||
var fullRef: String? = fullReference ?: return null
|
||||
|
||||
fullRef = fullRef!!.toUpperCase(Locale.ENGLISH)
|
||||
if (!fullRef.startsWith(STR_REF_START) || !fullRef.endsWith(STR_REF_END)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val ref = fullRef.substring(STR_REF_START.length, fullRef.length - STR_REF_START.length - STR_REF_END.length)
|
||||
if (ref.length <= 4) {
|
||||
return null
|
||||
}
|
||||
if (ref[1] != '@') {
|
||||
return null
|
||||
}
|
||||
if (ref[3] != ':') {
|
||||
return null
|
||||
}
|
||||
|
||||
val scan = Character.toUpperCase(ref[2])
|
||||
val wanted = Character.toUpperCase(ref[0])
|
||||
|
||||
val searchParametersV4 = SearchParametersV4()
|
||||
searchParametersV4.setupNone()
|
||||
|
||||
searchParametersV4.searchString = ref.substring(4)
|
||||
if (scan == 'T') {
|
||||
searchParametersV4.searchInTitles = true
|
||||
} else if (scan == 'U') {
|
||||
searchParametersV4.searchInUserNames = true
|
||||
} else if (scan == 'A') {
|
||||
searchParametersV4.searchInUrls = true
|
||||
} else if (scan == 'P') {
|
||||
searchParametersV4.searchInPasswords = true
|
||||
} else if (scan == 'N') {
|
||||
searchParametersV4.searchInNotes = true
|
||||
} else if (scan == 'I') {
|
||||
searchParametersV4.searchInUUIDs = true
|
||||
} else if (scan == 'O') {
|
||||
searchParametersV4.searchInOther = true
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
val list = ArrayList<PwEntryV4>()
|
||||
// TODO type parameter
|
||||
searchEntries(contextV4.databaseV4!!.rootGroup, searchParametersV4, list)
|
||||
|
||||
return if (list.size > 0) {
|
||||
TargetResult(list[0], wanted)
|
||||
} else null
|
||||
|
||||
}
|
||||
|
||||
private fun addRefsToCache(ref: String?, value: String?, ctx: SprContextV4?) {
|
||||
if (ref == null) {
|
||||
return
|
||||
}
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
if (ctx == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.refsCache.containsKey(ref)) {
|
||||
ctx.refsCache[ref] = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillRefsUsingCache(text: String, sprContextV4: SprContextV4): String {
|
||||
var newText = text
|
||||
for ((key, value) in sprContextV4.refsCache) {
|
||||
newText = StringUtil.replaceAllIgnoresCase(text, key, value, Locale.ENGLISH)
|
||||
}
|
||||
return newText
|
||||
}
|
||||
|
||||
private fun searchEntries(root: PwGroupV4?, searchParametersV4: SearchParametersV4?, listStorage: MutableList<PwEntryV4>?) {
|
||||
if (searchParametersV4 == null) {
|
||||
return
|
||||
}
|
||||
if (listStorage == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val terms = StringUtil.splitStringTerms(searchParametersV4.searchString)
|
||||
if (terms.size <= 1 || searchParametersV4.regularExpression) {
|
||||
root!!.doForEachChild(EntrySearchHandlerV4(searchParametersV4, listStorage), null)
|
||||
return
|
||||
}
|
||||
|
||||
// Search longest term first
|
||||
val stringLengthComparator = Comparator<String> { lhs, rhs -> lhs.length - rhs.length }
|
||||
Collections.sort(terms, stringLengthComparator)
|
||||
|
||||
val fullSearch = searchParametersV4.searchString
|
||||
var childEntries: List<PwEntryV4>? = root!!.getChildEntries()
|
||||
for (i in terms.indices) {
|
||||
val pgNew = ArrayList<PwEntryV4>()
|
||||
|
||||
searchParametersV4.searchString = terms[i]
|
||||
|
||||
var negate = false
|
||||
if (searchParametersV4.searchString.startsWith("-")) {
|
||||
searchParametersV4.searchString = searchParametersV4.searchString.substring(1)
|
||||
negate = searchParametersV4.searchString.isNotEmpty()
|
||||
}
|
||||
|
||||
if (!root.doForEachChild(EntrySearchHandlerV4(searchParametersV4, pgNew), null)) {
|
||||
childEntries = null
|
||||
break
|
||||
}
|
||||
|
||||
val complement = ArrayList<PwEntryV4>()
|
||||
if (negate) {
|
||||
for (entry in childEntries!!) {
|
||||
if (!pgNew.contains(entry)) {
|
||||
complement.add(entry)
|
||||
}
|
||||
}
|
||||
childEntries = complement
|
||||
} else {
|
||||
childEntries = pgNew
|
||||
}
|
||||
}
|
||||
|
||||
if (childEntries != null) {
|
||||
listStorage.addAll(childEntries)
|
||||
}
|
||||
searchParametersV4.searchString = fullSearch
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_RECURSION_DEPTH = 12
|
||||
private const val STR_REF_START = "{REF:"
|
||||
private const val STR_REF_END = "}"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
@@ -20,14 +20,8 @@
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
/**
|
||||
* Created by bpellin on 3/14/16.
|
||||
*/
|
||||
class ContentFileNotFoundException : FileNotFoundException() {
|
||||
companion object {
|
||||
fun getInstance(uri: Uri?): FileNotFoundException {
|
||||
@@ -36,11 +30,11 @@ class ContentFileNotFoundException : FileNotFoundException() {
|
||||
}
|
||||
|
||||
val scheme = uri.scheme
|
||||
|
||||
return if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("content", ignoreCase = true)) {
|
||||
return if (scheme != null
|
||||
&& scheme.isNotEmpty()
|
||||
&& scheme.equals("content", ignoreCase = true)) {
|
||||
ContentFileNotFoundException()
|
||||
} else FileNotFoundException()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user