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
|
* Add lock button always visible
|
||||||
* Refactor connection workflow
|
* Refactor connection workflow
|
||||||
* Better Magikeyboard connection
|
* Better Magikeyboard connection
|
||||||
|
* Kotlinized code
|
||||||
|
* Fix Recycle Bin
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta18)
|
KeepassDX (2.5.0.0beta18)
|
||||||
* New recent databases views
|
* New recent databases views
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
|
|||||||
|
|
||||||
## License
|
## 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.
|
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.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -19,18 +19,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests;
|
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.Context;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils;
|
import java.io.File;
|
||||||
import com.kunzisoft.keepass.utils.UriUtil;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
public class TestUtil {
|
public class TestUtil {
|
||||||
private static final File sdcard = Environment.getExternalStorageDirectory();
|
private static final File sdcard = Environment.getExternalStorageDirectory();
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ public class AESTest extends TestCase {
|
|||||||
mRand.nextBytes(ivArray);
|
mRand.nextBytes(ivArray);
|
||||||
IvParameterSpec iv = new IvParameterSpec(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);
|
android.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
byte[] outAndroid = android.doFinal(input, 0, dataSize);
|
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);
|
nat.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class CipherTest extends TestCase {
|
|||||||
rand.nextBytes(iv);
|
rand.nextBytes(iv);
|
||||||
rand.nextBytes(plaintext);
|
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 encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_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(iv);
|
||||||
rand.nextBytes(plaintext);
|
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 encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_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.content.res.AssetManager;
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDatabase;
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
||||||
import com.kunzisoft.keepass.database.element.PwEntryV4;
|
import com.kunzisoft.keepass.database.element.SprEngineV4;
|
||||||
import com.kunzisoft.keepass.utils.SprEngineV4;
|
|
||||||
import com.kunzisoft.keepass.utils.Types;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import biz.source_code.base64Coder.Base64Coder;
|
|
||||||
|
|
||||||
public class SprEngineTest extends AndroidTestCase {
|
public class SprEngineTest extends AndroidTestCase {
|
||||||
private PwDatabaseV4 db;
|
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>
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.database.action.DatabaseTaskNotificationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
<!-- Receiver for Keyboard -->
|
<!-- Receiver for Keyboard -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.receiver.NotificationDeleteBroadcastReceiver"
|
android:name="com.kunzisoft.keepass.magikeyboard.receiver.NotificationDeleteBroadcastReceiver"
|
||||||
|
|||||||
@@ -42,9 +42,8 @@ class AboutActivity : StylishActivity() {
|
|||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
toolbar.title = getString(R.string.menu_about)
|
toolbar.title = getString(R.string.menu_about)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
assert(supportActionBar != null)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
supportActionBar!!.setDisplayShowHomeEnabled(true)
|
|
||||||
|
|
||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.view.EntryContentsView
|
|||||||
import com.kunzisoft.keepass.app.App
|
import com.kunzisoft.keepass.app.App
|
||||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.notifications.NotificationEntryCopyManager
|
import com.kunzisoft.keepass.notifications.NotificationEntryCopyManager
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
|
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
|
||||||
@@ -140,7 +141,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
val database = App.currentDatabase
|
val database = App.currentDatabase
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
database.drawFactory.assignDatabaseIconTo(this, titleIconView, entry.icon, iconColor)
|
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
||||||
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
titleView?.text = entry.getVisualTitle()
|
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
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment.Companion.KEY_ICON_STANDARD
|
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment.Companion.KEY_ICON_STANDARD
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||||
import com.kunzisoft.keepass.view.EntryEditCustomField
|
|
||||||
import com.kunzisoft.keepass.app.App
|
import com.kunzisoft.keepass.app.App
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||||
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
|
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.*
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
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.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.Util
|
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 {
|
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
|
||||||
|
|
||||||
@@ -140,7 +143,9 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
mEntry = mDatabase?.createEntry()
|
mEntry = mDatabase?.createEntry()
|
||||||
mParent = mDatabase?.getGroupById(it)
|
mParent = mDatabase?.getGroupById(it)
|
||||||
// Add the default icon
|
// 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
|
// Close the activity if entry or parent can't be retrieve
|
||||||
@@ -252,7 +257,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
var actionRunnable: ActionRunnable? = null
|
var actionRunnable: ActionRunnable? = null
|
||||||
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
||||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||||
if (actionNodeValues.success)
|
if (actionNodeValues.result.isSuccess)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +311,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
for (i in 0 until it.childCount) {
|
for (i in 0 until it.childCount) {
|
||||||
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
|
||||||
val key = entryEditCustomField.label
|
val key = entryEditCustomField.label
|
||||||
if (key == null || key.isEmpty()) {
|
if (key.isEmpty()) {
|
||||||
validationErrorMessageId = R.string.error_string_key
|
validationErrorMessageId = R.string.error_string_key
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -392,12 +397,8 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun assignIconView() {
|
private fun assignIconView() {
|
||||||
mEntry?.icon?.let {
|
if (mDatabase?.drawFactory != null && mEntry?.icon != null) {
|
||||||
mDatabase?.drawFactory?.assignDatabaseIconTo(
|
entryIconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, mEntry?.icon!!, iconColor)
|
||||||
this,
|
|
||||||
entryIconView,
|
|
||||||
it,
|
|
||||||
iconColor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,10 +419,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
|
|||||||
|
|
||||||
val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this)
|
val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this)
|
||||||
if (visibilityFontActivated) {
|
if (visibilityFontActivated) {
|
||||||
Util.applyFontVisibilityTo(this, entryUserNameView)
|
entryUserNameView?.applyFontVisibility()
|
||||||
Util.applyFontVisibilityTo(this, entryPasswordView)
|
entryPasswordView?.applyFontVisibility()
|
||||||
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView)
|
entryConfirmationPasswordView?.applyFontVisibility()
|
||||||
Util.applyFontVisibilityTo(this, entryCommentView)
|
entryCommentView?.applyFontVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.allowExtraFields()) {
|
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.helpers.KeyFileHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
|
import com.kunzisoft.keepass.app.App
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.*
|
import com.kunzisoft.keepass.database.action.*
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
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.magikeyboard.KeyboardHelper
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import net.cachapa.expandablelayout.ExpandableLayout
|
import net.cachapa.expandablelayout.ExpandableLayout
|
||||||
@@ -170,13 +170,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
|
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
|
||||||
|
|
||||||
if (fileName!!.isNotEmpty()) {
|
if (fileName != null && fileName.isNotEmpty()) {
|
||||||
val dbUri = UriUtil.parseDefaultFile(fileName)
|
val dbUri = UriUtil.parseUriFile(fileName)
|
||||||
var scheme: String? = null
|
var scheme: String? = null
|
||||||
if (dbUri != null)
|
if (dbUri != null)
|
||||||
scheme = dbUri.scheme
|
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 path = dbUri!!.path
|
||||||
val db = File(path!!)
|
val db = File(path!!)
|
||||||
|
|
||||||
@@ -404,23 +404,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.path?.let { databaseFilename ->
|
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
|
||||||
// Create the new database and start prof
|
|
||||||
|
// Create the new database
|
||||||
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
||||||
{
|
{
|
||||||
CreateDatabaseRunnable(databaseFilename) { database ->
|
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
|
||||||
// TODO store database created
|
databaseUri,
|
||||||
AssignPasswordInDatabaseRunnable(
|
App.currentDatabase,
|
||||||
this@FileDatabaseSelectActivity,
|
|
||||||
database,
|
|
||||||
masterPasswordChecked,
|
masterPasswordChecked,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
keyFileChecked,
|
keyFileChecked,
|
||||||
keyFile,
|
keyFile,
|
||||||
true, // TODO get readonly
|
true, // TODO get readonly
|
||||||
LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename))
|
LaunchGroupActivityFinish(databaseUri)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
R.string.progress_create)
|
R.string.progress_create)
|
||||||
.start()
|
.start()
|
||||||
@@ -432,7 +430,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
// TODO remove
|
// TODO remove
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
|
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
|
||||||
@@ -441,9 +438,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
finishRun(true, null)
|
finishRun(true, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
override fun onFinishRun(result: Result) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Add database to recent files
|
// Add database to recent files
|
||||||
mFileDatabaseHistory?.addDatabaseUri(fileURI)
|
mFileDatabaseHistory?.addDatabaseUri(fileURI)
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
|||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
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.action.node.*
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
@@ -193,16 +193,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
// Add listeners to the add buttons
|
// Add listeners to the add buttons
|
||||||
addNodeButtonView?.setAddGroupClickListener {
|
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
||||||
GroupEditDialogFragment.build()
|
GroupEditDialogFragment.build()
|
||||||
.show(supportFragmentManager,
|
.show(supportFragmentManager,
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
}
|
})
|
||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// Search suggestion
|
// Search suggestion
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
@@ -348,7 +348,8 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Assign the group icon depending of IconPack or custom icon
|
// Assign the group icon depending of IconPack or custom icon
|
||||||
iconView?.visibility = View.VISIBLE
|
iconView?.visibility = View.VISIBLE
|
||||||
mCurrentGroup?.let {
|
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 (toolbar != null) {
|
||||||
if (mCurrentGroup?.containsParent() == true)
|
if (mCurrentGroup?.containsParent() == true)
|
||||||
@@ -408,7 +409,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
EntryActivity.launch(this@GroupActivity, entry, readOnly)
|
EntryActivity.launch(this@GroupActivity, entry, readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
MagikIME.setEntryKey(getEntry(entry))
|
MagikIME.entryKey = getEntry(entry)
|
||||||
// Show the notification if allowed in Preferences
|
// Show the notification if allowed in Preferences
|
||||||
if (PreferencesUtil.enableKeyboardNotificationEntry(this@GroupActivity)) {
|
if (PreferencesUtil.enableKeyboardNotificationEntry(this@GroupActivity)) {
|
||||||
startService(Intent(
|
startService(Intent(
|
||||||
@@ -442,7 +443,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
if (entry.containsCustomFields()) {
|
if (entry.containsCustomFields()) {
|
||||||
entry.fields
|
entry.fields
|
||||||
.doActionToAllCustomProtectedField { key, value ->
|
.doActionToAllCustomProtectedField { key, value ->
|
||||||
entryModel.addCustomField(
|
entryModel.customFields.add(
|
||||||
Field(key, value.toString()))
|
Field(key, value.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,12 +604,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
mSearchSuggestionAdapter?.reInit(this)
|
mSearchSuggestionAdapter?.reInit(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
// Hide button
|
|
||||||
addNodeButtonView?.hideButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
@@ -661,10 +656,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
// If no node, show education to add new one
|
// If no node, show education to add new one
|
||||||
if (mListNodesFragment != null
|
if (mListNodesFragment != null
|
||||||
&& mListNodesFragment!!.isEmpty
|
&& mListNodesFragment!!.isEmpty
|
||||||
&& addNodeButtonView != null
|
&& addNodeButtonView?.addButtonView != null
|
||||||
&& addNodeButtonView!!.isEnable
|
&& addNodeButtonView!!.isEnable
|
||||||
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
||||||
addNodeButtonView!!,
|
addNodeButtonView?.addButtonView!!,
|
||||||
{
|
{
|
||||||
addNodeButtonView?.openButtonIfClose()
|
addNodeButtonView?.openButtonIfClose()
|
||||||
},
|
},
|
||||||
@@ -829,7 +824,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
|
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (actionNodeValues.success) {
|
if (actionNodeValues.result.isSuccess) {
|
||||||
if (actionNodeValues.newNode != null)
|
if (actionNodeValues.newNode != null)
|
||||||
mListNodesFragment?.addNode(actionNodeValues.newNode)
|
mListNodesFragment?.addNode(actionNodeValues.newNode)
|
||||||
}
|
}
|
||||||
@@ -840,7 +835,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
|
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (actionNodeValues.success) {
|
if (actionNodeValues.result.isSuccess) {
|
||||||
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
|
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
|
||||||
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
|
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
|
||||||
}
|
}
|
||||||
@@ -851,22 +846,20 @@ class GroupActivity : LockingActivity(),
|
|||||||
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
|
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (actionNodeValues.success) {
|
if (actionNodeValues.result.isSuccess) {
|
||||||
if (actionNodeValues.oldNode != null)
|
|
||||||
mListNodesFragment?.removeNode(actionNodeValues.oldNode)
|
|
||||||
|
|
||||||
actionNodeValues.oldNode?.let { oldNode ->
|
actionNodeValues.oldNode?.let { oldNode ->
|
||||||
oldNode.parent?.let { parent ->
|
|
||||||
val database = App.currentDatabase
|
mListNodesFragment?.removeNode(oldNode)
|
||||||
if (database.isRecycleBinAvailable && database.isRecycleBinEnabled) {
|
|
||||||
val recycleBin = database.recycleBin
|
// TODO Move trash view
|
||||||
// Add trash if it doesn't exists
|
// Add trash in views list if it doesn't exists
|
||||||
if (parent == recycleBin
|
val database = App.currentDatabase
|
||||||
&& mCurrentGroup != null
|
if (database.isRecycleBinEnabled) {
|
||||||
&& mCurrentGroup!!.parent == null
|
val recycleBin = database.recycleBin
|
||||||
&& mCurrentGroup != recycleBin) {
|
if (mCurrentGroup != null && recycleBin != null
|
||||||
mListNodesFragment?.addNode(parent)
|
&& mCurrentGroup!!.parent == null
|
||||||
}
|
&& mCurrentGroup != recycleBin) {
|
||||||
|
mListNodesFragment?.addNode(recycleBin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -877,7 +870,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||||
name: String?,
|
name: String?,
|
||||||
iconId: PwIcon?) {
|
icon: PwIcon?) {
|
||||||
// Do nothing here
|
// Do nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,11 +906,15 @@ class GroupActivity : LockingActivity(),
|
|||||||
true)
|
true)
|
||||||
}
|
}
|
||||||
// Show the progress dialog now or after dialog confirmation
|
// Show the progress dialog now or after dialog confirmation
|
||||||
if (database.validatePasswordEncoding(masterPassword!!)) {
|
if (database.validatePasswordEncoding(masterPassword)) {
|
||||||
progressDialogThread.start()
|
progressDialogThread.start()
|
||||||
} else {
|
} else {
|
||||||
PasswordEncodingDialogHelper()
|
PasswordEncodingDialogFragment().apply {
|
||||||
.show(this, DialogInterface.OnClickListener{ _, _ -> progressDialogThread.start() })
|
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
|
// Not directly get the entry from intent data but from database
|
||||||
// Is refresh from onResume()
|
mListNodesFragment?.rebuildList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@@ -976,7 +973,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Else lock if needed
|
// Else lock if needed
|
||||||
else {
|
else {
|
||||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||||
App.currentDatabase.closeAndClear(applicationContext)
|
App.currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
} else {
|
} else {
|
||||||
moveTaskToBack(true)
|
moveTaskToBack(true)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mAdapter = NodeAdapter(getContextThemed(), currentActivity.menuInflater)
|
mAdapter = NodeAdapter(contextThemed, currentActivity.menuInflater)
|
||||||
mAdapter?.apply {
|
mAdapter?.apply {
|
||||||
setReadOnly(readOnly)
|
setReadOnly(readOnly)
|
||||||
setIsASearchResult(isASearchResult)
|
setIsASearchResult(isASearchResult)
|
||||||
@@ -115,7 +115,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
// To apply theme
|
// To apply theme
|
||||||
val rootView = inflater.cloneInContext(getContextThemed())
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
.inflate(R.layout.list_nodes_fragment, container, false)
|
.inflate(R.layout.list_nodes_fragment, container, false)
|
||||||
listView = rootView.findViewById(R.id.nodes_list)
|
listView = rootView.findViewById(R.id.nodes_list)
|
||||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||||
@@ -129,14 +129,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rebuildList()
|
||||||
|
|
||||||
return rootView
|
return rootView
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
rebuildList()
|
|
||||||
|
|
||||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||||
// To show the " no search entry found "
|
// To show the " no search entry found "
|
||||||
listView?.visibility = View.GONE
|
listView?.visibility = View.GONE
|
||||||
@@ -186,26 +186,28 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
when (item?.itemId) {
|
when (item?.itemId) {
|
||||||
|
|
||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
val sortDialogFragment: SortDialogFragment
|
context?.let { context ->
|
||||||
|
val sortDialogFragment: SortDialogFragment
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// TODO Recycle bin bottom
|
// TODO Recycle bin bottom
|
||||||
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
|
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
|
||||||
sortDialogFragment =
|
sortDialogFragment =
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PrefsUtil.getListSort(this),
|
PrefsUtil.getListSort(this),
|
||||||
PrefsUtil.getAscendingSort(this),
|
PrefsUtil.getAscendingSort(this),
|
||||||
PrefsUtil.getGroupsBeforeSort(this),
|
PrefsUtil.getGroupsBeforeSort(this),
|
||||||
PrefsUtil.getRecycleBinBottomSort(this));
|
PrefsUtil.getRecycleBinBottomSort(this));
|
||||||
} else {
|
} else {
|
||||||
*/
|
*/
|
||||||
sortDialogFragment = SortDialogFragment.getInstance(
|
sortDialogFragment = SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context))
|
PreferencesUtil.getGroupsBeforeSort(context))
|
||||||
//}
|
//}
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.kunzisoft.keepass.R
|
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.helpers.*
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
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.magikeyboard.KeyboardHelper
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import permissions.dispatcher.*
|
import permissions.dispatcher.*
|
||||||
@@ -265,8 +264,8 @@ class PasswordActivity : StylishActivity(),
|
|||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
|
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
|
||||||
if (mDatabaseFileUri != null
|
if (mDatabaseFileUri != null
|
||||||
&& !EmptyUtils.isNullOrEmpty(mDatabaseFileUri!!.path)
|
&& mDatabaseFileUri!!.path != null && mDatabaseFileUri!!.path!!.isNotEmpty()
|
||||||
&& UriUtil.equalsDefaultfile(mDatabaseFileUri, defaultFilename)) {
|
&& mDatabaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
|
||||||
checkboxDefaultDatabaseView?.isChecked = true
|
checkboxDefaultDatabaseView?.isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,9 +525,9 @@ class PasswordActivity : StylishActivity(),
|
|||||||
setFingerPrintView(R.string.encrypted_value_stored)
|
setFingerPrintView(R.string.encrypted_value_stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDecryptedResult(passwordValue: String) {
|
override fun handleDecryptedResult(value: String) {
|
||||||
// Load database directly
|
// Load database directly
|
||||||
verifyKeyFileViewsAndLoadDatabase(passwordValue)
|
verifyKeyFileViewsAndLoadDatabase(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
@@ -563,8 +562,8 @@ class PasswordActivity : StylishActivity(),
|
|||||||
|
|
||||||
private fun verifyAllViewsAndLoadDatabase() {
|
private fun verifyAllViewsAndLoadDatabase() {
|
||||||
verifyCheckboxesAndLoadDatabase(
|
verifyCheckboxesAndLoadDatabase(
|
||||||
passwordView?.text.toString(),
|
passwordView?.text?.toString(),
|
||||||
UriUtil.parseDefaultFile(keyFileView?.text.toString()))
|
UriUtil.parseUriFile(keyFileView?.text?.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(password: String?, keyFile: Uri?) {
|
private fun verifyCheckboxesAndLoadDatabase(password: String?, keyFile: Uri?) {
|
||||||
@@ -580,8 +579,8 @@ class PasswordActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyKeyFileViewsAndLoadDatabase(password: String) {
|
private fun verifyKeyFileViewsAndLoadDatabase(password: String) {
|
||||||
val key = keyFileView?.text.toString()
|
val key = keyFileView?.text?.toString()
|
||||||
var keyUri = UriUtil.parseDefaultFile(key)
|
var keyUri = UriUtil.parseUriFile(key)
|
||||||
if (checkboxKeyFileView?.isChecked != true) {
|
if (checkboxKeyFileView?.isChecked != true) {
|
||||||
keyUri = null
|
keyUri = null
|
||||||
}
|
}
|
||||||
@@ -591,20 +590,20 @@ class PasswordActivity : StylishActivity(),
|
|||||||
private fun loadDatabase(password: String?, keyFile: Uri?) {
|
private fun loadDatabase(password: String?, keyFile: Uri?) {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
val database = App.currentDatabase
|
val database = App.currentDatabase
|
||||||
database.closeAndClear(applicationContext)
|
database.closeAndClear(applicationContext.filesDir)
|
||||||
|
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog and load the database
|
// Show the progress dialog and load the database
|
||||||
ProgressDialogThread(this,
|
ProgressDialogThread(this,
|
||||||
{ progressTaskUpdater ->
|
{ progressTaskUpdater ->
|
||||||
LoadDatabaseRunnable(
|
LoadDatabaseRunnable(
|
||||||
WeakReference(this@PasswordActivity.applicationContext),
|
WeakReference(this@PasswordActivity),
|
||||||
database,
|
database,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
password,
|
||||||
keyFile,
|
keyFile,
|
||||||
progressTaskUpdater,
|
progressTaskUpdater,
|
||||||
AfterLoadingDatabase(database))
|
AfterLoadingDatabase(database, password))
|
||||||
},
|
},
|
||||||
R.string.loading_database).start()
|
R.string.loading_database).start()
|
||||||
}
|
}
|
||||||
@@ -613,9 +612,11 @@ class PasswordActivity : StylishActivity(),
|
|||||||
/**
|
/**
|
||||||
* Called after verify and try to opening the database
|
* 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 {
|
runOnUiThread {
|
||||||
// Recheck fingerprint if error
|
// Recheck fingerprint if error
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -623,15 +624,20 @@ class PasswordActivity : StylishActivity(),
|
|||||||
reInitWithFingerprintMode()
|
reInitWithFingerprintMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (database.isPasswordEncodingError) {
|
if (result.isSuccess) {
|
||||||
val dialog = PasswordEncodingDialogHelper()
|
if (database.validatePasswordEncoding(password)) {
|
||||||
dialog.show(this@PasswordActivity,
|
launchGroupActivity()
|
||||||
DialogInterface.OnClickListener { _, _ -> launchGroupActivity() })
|
} else {
|
||||||
} else if (isSuccess) {
|
PasswordEncodingDialogFragment().apply {
|
||||||
launchGroupActivity()
|
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||||
|
launchGroupActivity()
|
||||||
|
}
|
||||||
|
show(supportFragmentManager, "passwordEncodingTag")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (message != null && message.isNotEmpty()) {
|
if (result.message != null && result.message!!.isNotEmpty()) {
|
||||||
Toast.makeText(this@PasswordActivity, message, Toast.LENGTH_LONG).show()
|
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,7 +768,7 @@ class PasswordActivity : StylishActivity(),
|
|||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
||||||
setEmptyViews()
|
setEmptyViews()
|
||||||
App.currentDatabase.closeAndClear(applicationContext)
|
App.currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -814,14 +820,13 @@ class PasswordActivity : StylishActivity(),
|
|||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
private fun verifyFileNameUriFromLaunch(fileName: String) {
|
private fun verifyFileNameUriFromLaunch(fileName: String) {
|
||||||
if (EmptyUtils.isNullOrEmpty(fileName)) {
|
if (fileName.isEmpty()) {
|
||||||
throw FileNotFoundException()
|
throw FileNotFoundException()
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = UriUtil.parseDefaultFile(fileName)
|
val uri = UriUtil.parseUriFile(fileName)
|
||||||
val scheme = uri.scheme
|
val scheme = uri?.scheme
|
||||||
|
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||||
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equals("file", ignoreCase = true)) {
|
|
||||||
val dbFile = File(uri.path!!)
|
val dbFile = File(uri.path!!)
|
||||||
if (!dbFile.exists()) {
|
if (!dbFile.exists()) {
|
||||||
throw FileNotFoundException()
|
throw FileNotFoundException()
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
@@ -178,13 +177,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
private fun verifyFile(): Boolean {
|
private fun verifyFile(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
&& keyFileView != null) {
|
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
|
||||||
val keyFile = UriUtil.parseDefaultFile(keyFileView!!.text.toString())
|
|
||||||
mKeyFile = keyFile
|
mKeyFile = keyFile
|
||||||
|
|
||||||
// Verify that a keyfile is set
|
// Verify that a keyfile is set
|
||||||
if (EmptyUtils.isNullOrEmpty(keyFile)) {
|
if (keyFile == null || keyFile.toString().isEmpty()) {
|
||||||
error = true
|
error = true
|
||||||
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
@@ -229,11 +227,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.let { currentUri ->
|
UriUtil.parseUriFile(uri)?.let { pathUri ->
|
||||||
UriUtil.parseDefaultFile(currentUri.toString())?.let { pathString ->
|
keyFileCheckBox?.isChecked = true
|
||||||
keyFileCheckBox?.isChecked = true
|
keyFileView?.text = pathUri.toString()
|
||||||
keyFileView?.text = pathString.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ class BrowserDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val market = root.findViewById<Button>(R.id.install_market)
|
val market = root.findViewById<Button>(R.id.install_market)
|
||||||
market.setOnClickListener {
|
market.setOnClickListener {
|
||||||
Util.gotoUrl(context, R.string.filemanager_play_store)
|
Util.gotoUrl(context!!, R.string.filemanager_play_store)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
val web = root.findViewById<Button>(R.id.install_web)
|
val web = root.findViewById<Button>(R.id.install_web)
|
||||||
web.setOnClickListener {
|
web.setOnClickListener {
|
||||||
Util.gotoUrl(context, R.string.filemanager_f_droid)
|
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
|
|||||||
|
|
||||||
// Spinner Drop down elements
|
// Spinner Drop down elements
|
||||||
val fileTypes = resources.getStringArray(R.array.file_types)
|
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)
|
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
spinner.adapter = dataAdapter
|
spinner.adapter = dataAdapter
|
||||||
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
|
// 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()
|
val dialog = builder.create()
|
||||||
|
|
||||||
dialog.setOnShowListener { dialog1 ->
|
dialog.setOnShowListener { _ ->
|
||||||
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||||
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
|
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
positiveButton?.setOnClickListener { _ ->
|
positiveButton?.setOnClickListener { _ ->
|
||||||
@@ -181,11 +181,13 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPath(): Uri? {
|
private fun buildPath(): Uri? {
|
||||||
if (folderPathView != null && mDatabaseFileExtension != null) {
|
if (folderPathView != null && fileNameView != null && mDatabaseFileExtension != null) {
|
||||||
var path = Uri.Builder().path(folderPathView!!.text.toString())
|
var path = Uri.Builder().path(folderPathView!!.text.toString())
|
||||||
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
|
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
|
||||||
.build()
|
.build()
|
||||||
path = UriUtil.translate(context, path)
|
context?.let { context ->
|
||||||
|
path = UriUtil.translateUri(context, path)
|
||||||
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.Util
|
||||||
|
import com.kunzisoft.keepass.utils.applyFontVisibility
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
root = inflater.inflate(R.layout.generate_password, null)
|
root = inflater.inflate(R.layout.generate_password, null)
|
||||||
|
|
||||||
passwordView = root?.findViewById(R.id.password)
|
passwordView = root?.findViewById(R.id.password)
|
||||||
Util.applyFontVisibilityTo(context, passwordView)
|
passwordView?.applyFontVisibility()
|
||||||
|
|
||||||
lengthTextView = root?.findViewById(R.id.length)
|
lengthTextView = root?.findViewById(R.id.length)
|
||||||
|
|
||||||
@@ -92,7 +93,10 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||||
})
|
})
|
||||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
|
||||||
|
context?.let { context ->
|
||||||
|
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
||||||
|
}
|
||||||
|
|
||||||
root?.findViewById<Button>(R.id.generate_password_button)
|
root?.findViewById<Button>(R.id.generate_password_button)
|
||||||
?.setOnClickListener { fillPassword() }
|
?.setOnClickListener { fillPassword() }
|
||||||
@@ -131,18 +135,21 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
bracketsBox?.isChecked = false
|
bracketsBox?.isChecked = false
|
||||||
extendedBox?.isChecked = false
|
extendedBox?.isChecked = false
|
||||||
|
|
||||||
val defaultPasswordChars = PreferencesUtil.getDefaultPasswordCharacters(context)
|
context?.let { context ->
|
||||||
for (passwordChar in defaultPasswordChars) {
|
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
||||||
when (passwordChar) {
|
for (passwordChar in charSet) {
|
||||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
when (passwordChar) {
|
||||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
||||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
||||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
||||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
||||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
||||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
||||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
||||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
||||||
|
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +163,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
try {
|
try {
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||||
|
|
||||||
val generator = PasswordGenerator(activity)
|
val generator = PasswordGenerator(resources)
|
||||||
password = generator.generatePassword(length,
|
password = generator.generatePassword(length,
|
||||||
uppercaseBox?.isChecked == true,
|
uppercaseBox?.isChecked == true,
|
||||||
lowercaseBox?.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.Database
|
||||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||||
import com.kunzisoft.keepass.database.element.PwIcon
|
import com.kunzisoft.keepass.database.element.PwIcon
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
private var nameGroup: String? = null
|
private var nameGroup: String? = null
|
||||||
private var iconGroup: PwIcon? = null
|
private var iconGroup: PwIcon? = null
|
||||||
|
|
||||||
private var iconButton: ImageView? = null
|
private var iconButtonView: ImageView? = null
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
enum class EditGroupDialogAction {
|
enum class EditGroupDialogAction {
|
||||||
@@ -76,7 +77,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.group_edit, null)
|
val root = activity.layoutInflater.inflate(R.layout.group_edit, null)
|
||||||
val nameField = root?.findViewById<TextView>(R.id.group_edit_name)
|
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
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
@@ -133,7 +134,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
this@GroupEditDialogFragment.dialog.cancel()
|
this@GroupEditDialogFragment.dialog.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
iconButton?.setOnClickListener { _ ->
|
iconButtonView?.setOnClickListener { _ ->
|
||||||
fragmentManager?.let {
|
fragmentManager?.let {
|
||||||
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
||||||
}
|
}
|
||||||
@@ -145,12 +146,9 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun assignIconView() {
|
private fun assignIconView() {
|
||||||
mDatabase?.drawFactory
|
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
||||||
?.assignDatabaseIconTo(
|
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
||||||
context,
|
}
|
||||||
iconButton,
|
|
||||||
iconGroup,
|
|
||||||
iconColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
override fun iconPicked(bundle: Bundle) {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(context)
|
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
// Inflate and set the layout for the dialog
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
// 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)
|
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
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 {
|
} 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)
|
builder.setView(rootView)
|
||||||
|
|||||||
@@ -20,25 +20,27 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class PasswordEncodingDialogHelper {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
private var dialog: AlertDialog? = null
|
|
||||||
|
|
||||||
@JvmOverloads
|
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||||
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)
|
|
||||||
|
|
||||||
if (showCancel) {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
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)))
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
try {
|
try {
|
||||||
Util.gotoUrl(context, R.string.app_pro_url)
|
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
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)))
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
try {
|
try {
|
||||||
Util.gotoUrl(context, R.string.contribution_url)
|
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
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 {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = AlertDialog.Builder(activity!!)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
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_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_work_hard))).append("\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
|
.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 {
|
} else {
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
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_buy_pro))).append("\n")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||||
builder.setPositiveButton(R.string.download) { dialog, id ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
try {
|
try {
|
||||||
Util.gotoUrl(context, R.string.app_pro_url)
|
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
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 {
|
} else {
|
||||||
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
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_contibute))).append(" ")
|
||||||
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||||
builder.setPositiveButton(R.string.contribute) { dialog, id ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
try {
|
try {
|
||||||
Util.gotoUrl(context, R.string.contribution_url)
|
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
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)
|
builder.setMessage(stringBuilder)
|
||||||
// Create the AlertDialog object and return it
|
// 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
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.support.v4.app.Fragment
|
import android.support.v4.app.Fragment
|
||||||
@@ -30,7 +32,6 @@ import android.util.Log
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||||
import com.kunzisoft.keepass.fileselect.StorageAF
|
import com.kunzisoft.keepass.fileselect.StorageAF
|
||||||
import com.kunzisoft.keepass.utils.Interaction
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class KeyFileHelper {
|
class KeyFileHelper {
|
||||||
@@ -55,7 +56,7 @@ class KeyFileHelper {
|
|||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
try {
|
try {
|
||||||
if (StorageAF.useStorageFramework(activity)) {
|
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
|
||||||
openActivityWithActionOpenDocument()
|
openActivityWithActionOpenDocument()
|
||||||
} else {
|
} else {
|
||||||
openActivityWithActionGetContent()
|
openActivityWithActionGetContent()
|
||||||
@@ -67,7 +68,6 @@ class KeyFileHelper {
|
|||||||
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
||||||
showBrowserDialog()
|
showBrowserDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class KeyFileHelper {
|
|||||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
||||||
var showBrowser = false
|
var showBrowser = false
|
||||||
try {
|
try {
|
||||||
if (Interaction.isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||||
// Get file path parent if possible
|
// Get file path parent if possible
|
||||||
if (dataUri != null
|
if (dataUri != null
|
||||||
@@ -130,6 +130,26 @@ class KeyFileHelper {
|
|||||||
return showBrowser
|
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
|
* Show Browser dialog to select file picker app
|
||||||
*/
|
*/
|
||||||
@@ -162,7 +182,7 @@ class KeyFileHelper {
|
|||||||
val filename = data?.dataString
|
val filename = data?.dataString
|
||||||
var keyUri: Uri? = null
|
var keyUri: Uri? = null
|
||||||
if (filename != null) {
|
if (filename != null) {
|
||||||
keyUri = UriUtil.parseDefaultFile(filename)
|
keyUri = UriUtil.parseUriFile(filename)
|
||||||
}
|
}
|
||||||
keyFileCallback?.invoke(keyUri)
|
keyFileCallback?.invoke(keyUri)
|
||||||
}
|
}
|
||||||
@@ -173,7 +193,7 @@ class KeyFileHelper {
|
|||||||
if (data != null) {
|
if (data != null) {
|
||||||
var uri = data.data
|
var uri = data.data
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
if (StorageAF.useStorageFramework(activity)) {
|
if (StorageAF.useStorageFramework(activity!!)) {
|
||||||
try {
|
try {
|
||||||
// try to persist read and write permissions
|
// try to persist read and write permissions
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
@@ -185,10 +205,9 @@ class KeyFileHelper {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if (requestCode == GET_CONTENT) {
|
if (requestCode == GET_CONTENT) {
|
||||||
uri = UriUtil.translate(activity, uri)
|
uri = UriUtil.translateUri(activity!!, uri)
|
||||||
}
|
}
|
||||||
keyFileCallback?.invoke(uri)
|
keyFileCallback?.invoke(uri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ class UriIntentInitTask(private val weakContext: WeakReference<Context>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
databaseUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_FILENAME))
|
databaseUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_FILENAME))
|
||||||
keyFileUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_KEYFILE))
|
keyFileUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_KEYFILE))
|
||||||
|
|
||||||
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
|
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
|
||||||
keyFileUri = getKeyFileUri(databaseUri)
|
keyFileUri = getKeyFileUri(databaseUri)
|
||||||
|
|||||||
@@ -71,13 +71,12 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutEnable) {
|
if (timeoutEnable) {
|
||||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this)) {
|
lockReceiver = LockReceiver()
|
||||||
lockReceiver = LockReceiver()
|
val intentFilter = IntentFilter().apply {
|
||||||
val intentFilter = IntentFilter()
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
intentFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
addAction(LOCK_ACTION)
|
||||||
intentFilter.addAction(LOCK_ACTION)
|
|
||||||
registerReceiver(lockReceiver, IntentFilter(intentFilter))
|
|
||||||
}
|
}
|
||||||
|
registerReceiver(lockReceiver, IntentFilter(intentFilter))
|
||||||
}
|
}
|
||||||
|
|
||||||
exitLock = false
|
exitLock = false
|
||||||
@@ -190,7 +189,7 @@ fun Activity.lock() {
|
|||||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||||
cancelAll()
|
cancelAll()
|
||||||
}
|
}
|
||||||
App.currentDatabase.closeAndClear(applicationContext)
|
App.currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||||
finish()
|
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.app.App
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.Util
|
import com.kunzisoft.keepass.utils.Util
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ class NodeAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun assignPreferences() {
|
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.textSize = PreferencesUtil.getListTextSize(context)
|
||||||
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
|
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
|
||||||
// Retrieve the icon size
|
// Retrieve the icon size
|
||||||
@@ -207,7 +208,7 @@ class NodeAdapter
|
|||||||
Type.GROUP -> iconGroupColor
|
Type.GROUP -> iconGroupColor
|
||||||
Type.ENTRY -> iconEntryColor
|
Type.ENTRY -> iconEntryColor
|
||||||
}
|
}
|
||||||
mDatabase.drawFactory.assignDatabaseIconTo(context, holder.icon, subNode.icon, iconColor)
|
holder.icon?.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||||
// Assign text
|
// Assign text
|
||||||
holder.text?.text = subNode.title
|
holder.text?.text = subNode.title
|
||||||
// Assign click
|
// 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.Database
|
||||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.PwIcon
|
import com.kunzisoft.keepass.database.element.PwIcon
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.util.*
|
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(
|
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
@@ -89,7 +91,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
|||||||
val viewHolder = view.tag as ViewHolder
|
val viewHolder = view.tag as ViewHolder
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
database.drawFactory.assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor)
|
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
|
||||||
|
|
||||||
// Assign title
|
// Assign title
|
||||||
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
|
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? {
|
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||||
return database.searchEntry(constraint.toString())
|
return database.searchEntries(constraint.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryFromPosition(position: Int): EntryVersioned? {
|
fun getEntryFromPosition(position: Int): EntryVersioned? {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class App : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
currentDatabase.closeAndClear(applicationContext)
|
currentDatabase.closeAndClear(applicationContext.filesDir)
|
||||||
super.onTerminate()
|
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.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
@@ -17,20 +17,18 @@
|
|||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
companion object {
|
||||||
*
|
|
||||||
*/
|
|
||||||
private static final long serialVersionUID = -3846349284296062658L;
|
|
||||||
|
|
||||||
public AESProvider() {
|
private const val serialVersionUID = -3846349284296062658L
|
||||||
super("AESProvider", 1.0, "");
|
|
||||||
put("Cipher.AES",NativeAESCipherSpi.class.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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.
|
* 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.
|
* 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 final String TAG = NativeAESCipherSpi.class.getName();
|
||||||
|
|
||||||
private static boolean mIsStaticInit = false;
|
private static boolean mIsStaticInit = false;
|
||||||
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<PhantomReference<NativeAESCipherSpi>, Long>();
|
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<>();
|
||||||
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<NativeAESCipherSpi>();
|
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<>();
|
||||||
|
|
||||||
private final int AES_BLOCK_SIZE = 16;
|
private final int AES_BLOCK_SIZE = 16;
|
||||||
private byte[] mIV;
|
private byte[] mIV;
|
||||||
|
|
||||||
private boolean mIsInited = false;
|
private boolean mIsInit = false;
|
||||||
private boolean mEncrypting = false;
|
|
||||||
private long mCtxPtr;
|
private long mCtxPtr;
|
||||||
|
|
||||||
private boolean mPadding = false;
|
private boolean mPadding = false;
|
||||||
@@ -68,13 +67,12 @@ public class NativeAESCipherSpi extends CipherSpi {
|
|||||||
|
|
||||||
private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) {
|
private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) {
|
||||||
Log.d(TAG, "queued cipher context: " + 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
|
/** Work with the garbage collector to clean up openssl memory when the cipher
|
||||||
* context is garbage collected.
|
* context is garbage collected.
|
||||||
* @author bpellin
|
* @author bpellin
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
private static class Cleanup implements Runnable {
|
private static class Cleanup implements Runnable {
|
||||||
|
|
||||||
@@ -92,13 +90,12 @@ public class NativeAESCipherSpi extends CipherSpi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native void nCleanup(long ctxPtr);
|
private static native void nCleanup(long ctxPtr);
|
||||||
|
|
||||||
public NativeAESCipherSpi() {
|
public NativeAESCipherSpi() {
|
||||||
if ( ! mIsStaticInit ) {
|
if ( !mIsStaticInit ) {
|
||||||
staticInit();
|
staticInit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,11 +131,9 @@ public class NativeAESCipherSpi extends CipherSpi {
|
|||||||
IllegalBlockSizeException, BadPaddingException {
|
IllegalBlockSizeException, BadPaddingException {
|
||||||
|
|
||||||
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
|
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
|
||||||
|
|
||||||
if ( result == -1 ) {
|
if ( result == -1 ) {
|
||||||
throw new ShortBufferException();
|
throw new ShortBufferException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +141,6 @@ public class NativeAESCipherSpi extends CipherSpi {
|
|||||||
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
|
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
|
||||||
|
|
||||||
int outputSize = engineGetOutputSize(inputLen);
|
int outputSize = engineGetOutputSize(inputLen);
|
||||||
|
|
||||||
int updateAmt;
|
int updateAmt;
|
||||||
if (input != null && inputLen > 0) {
|
if (input != null && inputLen > 0) {
|
||||||
updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
|
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 finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt);
|
||||||
|
return updateAmt + finalAmt;
|
||||||
int out = updateAmt + finalAmt;
|
|
||||||
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize)
|
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) {
|
} catch (InvalidParameterSpecException e) {
|
||||||
throw new InvalidAlgorithmParameterException(e);
|
throw new InvalidAlgorithmParameterException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init(int opmode, Key key, IvParameterSpec params) {
|
private void init(int opmode, Key key, IvParameterSpec params) {
|
||||||
if ( mIsInited ) {
|
if (mIsInit) {
|
||||||
// Do not allow multiple inits
|
// Do not allow multiple inits
|
||||||
assert(true);
|
|
||||||
throw new RuntimeException("Don't allow multiple inits");
|
throw new RuntimeException("Don't allow multiple inits");
|
||||||
} else {
|
} else {
|
||||||
NativeLib.init();
|
NativeLib.INSTANCE.init();
|
||||||
mIsInited = true;
|
mIsInit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
mIV = params.getIV();
|
mIV = params.getIV();
|
||||||
mEncrypting = opmode == Cipher.ENCRYPT_MODE;
|
mCtxPtr = nInit(opmode == Cipher.ENCRYPT_MODE, key.getEncoded(), mIV);
|
||||||
mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV);
|
|
||||||
addToCleanupQueue(this, mCtxPtr);
|
addToCleanupQueue(this, mCtxPtr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,26 +250,23 @@ public class NativeAESCipherSpi extends CipherSpi {
|
|||||||
protected void engineSetPadding(String padding)
|
protected void engineSetPadding(String padding)
|
||||||
throws NoSuchPaddingException {
|
throws NoSuchPaddingException {
|
||||||
|
|
||||||
if ( ! mIsInited ) {
|
if ( !mIsInit) {
|
||||||
NativeLib.init();
|
NativeLib.INSTANCE.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( padding.length() == 0 ) {
|
if ( padding.length() == 0 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ( !padding.equals("PKCS5Padding") ) {
|
||||||
if ( ! padding.equals("PKCS5Padding") ) {
|
|
||||||
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
|
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
|
||||||
}
|
}
|
||||||
|
|
||||||
mPadding = true;
|
mPadding = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
|
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
|
||||||
int maxSize = engineGetOutputSize(inputLen);
|
int maxSize = engineGetOutputSize(inputLen);
|
||||||
byte output[] = new byte[maxSize];
|
byte[] output = new byte[maxSize];
|
||||||
|
|
||||||
int updateSize = update(input, inputOffset, inputLen, output, 0);
|
int updateSize = update(input, inputOffset, inputLen, output, 0);
|
||||||
|
|
||||||
@@ -302,24 +286,15 @@ public class NativeAESCipherSpi extends CipherSpi {
|
|||||||
byte[] output, int outputOffset) throws ShortBufferException {
|
byte[] output, int outputOffset) throws ShortBufferException {
|
||||||
|
|
||||||
int result = update(input, inputOffset, inputLen, output, outputOffset);
|
int result = update(input, inputOffset, inputLen, output, outputOffset);
|
||||||
|
|
||||||
if ( result == -1 ) {
|
if ( result == -1 ) {
|
||||||
throw new ShortBufferException("Insufficient buffer.");
|
throw new ShortBufferException("Insufficient buffer.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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 outputSize = engineGetOutputSize(inputLen);
|
||||||
|
return nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
|
||||||
int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
|
|
||||||
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int 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.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
@@ -17,30 +17,30 @@
|
|||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* 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 {
|
object NativeLib {
|
||||||
private static boolean isLoaded = false;
|
private var isLoaded = false
|
||||||
private static boolean loadSuccess = false;
|
private var loadSuccess = false
|
||||||
|
|
||||||
public static boolean loaded() {
|
fun loaded(): Boolean {
|
||||||
return init();
|
return init()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean init() {
|
fun init(): Boolean {
|
||||||
if ( ! isLoaded ) {
|
if (!isLoaded) {
|
||||||
try {
|
try {
|
||||||
System.loadLibrary("final-key");
|
System.loadLibrary("final-key")
|
||||||
System.loadLibrary("argon2");
|
System.loadLibrary("argon2")
|
||||||
} catch ( UnsatisfiedLinkError e) {
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
isLoaded = true;
|
|
||||||
loadSuccess = true;
|
isLoaded = true
|
||||||
|
loadSuccess = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadSuccess;
|
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 class FinalKeyFactory {
|
||||||
public static FinalKey createFinalKey() {
|
public static FinalKey createFinalKey() {
|
||||||
return createFinalKey(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FinalKey createFinalKey(boolean androidOverride) {
|
|
||||||
// Prefer the native final key implementation
|
// Prefer the native final key implementation
|
||||||
if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) {
|
if ( !CipherFactory.INSTANCE.deviceBlacklisted() && NativeFinalKey.availble() ) {
|
||||||
return new NativeFinalKey();
|
return new NativeFinalKey();
|
||||||
} else {
|
} else {
|
||||||
// Fall back on the android crypto implementation
|
// Fall back on the android crypto implementation
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import java.io.IOException;
|
|||||||
public class NativeFinalKey extends FinalKey {
|
public class NativeFinalKey extends FinalKey {
|
||||||
|
|
||||||
public static boolean availble() {
|
public static boolean availble() {
|
||||||
return NativeLib.init();
|
return NativeLib.INSTANCE.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
|
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
|
||||||
NativeLib.init();
|
NativeLib.INSTANCE.init();
|
||||||
|
|
||||||
return nTransformMasterKey(seed, key, rounds);
|
return nTransformMasterKey(seed, key, rounds);
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ public class AesKdf extends KdfEngine {
|
|||||||
byte[] seed = p.getByteArray(ParamSeed);
|
byte[] seed = p.getByteArray(ParamSeed);
|
||||||
|
|
||||||
if (masterKey.length != 32) {
|
if (masterKey.length != 32) {
|
||||||
masterKey = CryptoUtil.hashSha256(masterKey);
|
masterKey = CryptoUtil.INSTANCE.hashSha256(masterKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seed.length != 32) {
|
if (seed.length != 32) {
|
||||||
seed = CryptoUtil.hashSha256(seed);
|
seed = CryptoUtil.INSTANCE.hashSha256(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
FinalKey key = FinalKeyFactory.createFinalKey();
|
FinalKey key = FinalKeyFactory.createFinalKey();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class Argon2Native {
|
|||||||
public static byte[] transformKey(byte[] password, byte[] salt, int parallelism,
|
public static byte[] transformKey(byte[] password, byte[] salt, int parallelism,
|
||||||
long memory, long iterations, byte[] secretKey,
|
long memory, long iterations, byte[] secretKey,
|
||||||
byte[] associatedData, long version) throws IOException {
|
byte[] associatedData, long version) throws IOException {
|
||||||
NativeLib.init();
|
NativeLib.INSTANCE.init();
|
||||||
|
|
||||||
return nTransformMasterKey(password, salt, parallelism, memory, iterations, secretKey, associatedData, version);
|
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.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
database: Database,
|
database: Database,
|
||||||
withMasterPassword: Boolean,
|
withMasterPassword: Boolean,
|
||||||
@@ -57,7 +57,7 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
|||||||
mBackupKey = ByteArray(database.masterKey.size)
|
mBackupKey = ByteArray(database.masterKey.size)
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.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)
|
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
||||||
|
|
||||||
// To save the database
|
// To save the database
|
||||||
@@ -72,8 +72,8 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
override fun onFinishRun(result: Result) {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// Erase the current master key
|
// Erase the current master key
|
||||||
erase(database.masterKey)
|
erase(database.masterKey)
|
||||||
mBackupKey?.let {
|
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
|
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.database.element.Database
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
class CreateDatabaseRunnable(private val mFilename: String,
|
class CreateDatabaseRunnable(context: Context,
|
||||||
val onDatabaseCreate: (database: Database) -> ActionRunnable)
|
private val mDatabaseUri: Uri,
|
||||||
: ActionRunnable() {
|
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() {
|
override fun run() {
|
||||||
try {
|
try {
|
||||||
// Create new database record
|
// Create new database record
|
||||||
Database(mFilename).apply {
|
mDatabase.apply {
|
||||||
App.currentDatabase = this
|
createData(mDatabaseUri)
|
||||||
// Set Database state
|
// Set Database state
|
||||||
loaded = true
|
loaded = true
|
||||||
// Commit changes
|
// Commit changes
|
||||||
onDatabaseCreate(this).run()
|
super.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
finishRun(true)
|
finishRun(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
mDatabase.closeAndClear()
|
||||||
finishRun(false, e.message)
|
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)
|
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 {
|
companion object {
|
||||||
private val TAG = LoadDatabaseRunnable::class.java.name
|
private val TAG = LoadDatabaseRunnable::class.java.name
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.kunzisoft.keepass.database.action
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
|
import android.os.Build
|
||||||
import android.support.annotation.StringRes
|
import android.support.annotation.StringRes
|
||||||
import android.support.v4.app.FragmentActivity
|
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.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
open class ProgressDialogThread(private val activity: FragmentActivity,
|
open class ProgressDialogThread(private val activity: FragmentActivity,
|
||||||
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
|
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
|
||||||
@@ -18,18 +22,31 @@ open class ProgressDialogThread(private val activity: FragmentActivity,
|
|||||||
messageId,
|
messageId,
|
||||||
warningId)
|
warningId)
|
||||||
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
|
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
|
||||||
|
var actionFinishInUIThread: ActionRunnable? = null
|
||||||
|
|
||||||
|
private var intentDatabaseTask:Intent = Intent(activity, DatabaseTaskNotificationService::class.java)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
|
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
|
||||||
{
|
{
|
||||||
activity.runOnUiThread {
|
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
|
// Show the dialog
|
||||||
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
|
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
|
||||||
}
|
}
|
||||||
}, {
|
}, { result ->
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
|
actionFinishInUIThread?.onFinishRun(result)
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
ProgressTaskDialogFragment.stop(activity)
|
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 class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
|
||||||
private val onPreExecute: () -> Unit,
|
private val onPreExecute: () -> Unit,
|
||||||
private val onPostExecute: () -> Unit)
|
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
|
||||||
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, Void>() {
|
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() {
|
||||||
|
|
||||||
override fun onPreExecute() {
|
override fun onPreExecute() {
|
||||||
super.onPreExecute()
|
super.onPreExecute()
|
||||||
onPreExecute.invoke()
|
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 {
|
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)
|
super.onPostExecute(result)
|
||||||
onPostExecute.invoke()
|
onPostExecute.invoke(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,23 +20,16 @@
|
|||||||
package com.kunzisoft.keepass.database.action
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException
|
import com.kunzisoft.keepass.database.exception.PwDbOutputException
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
open class SaveDatabaseRunnable(protected var context: Context,
|
abstract class SaveDatabaseRunnable(protected var context: Context,
|
||||||
protected var database: Database,
|
protected var database: Database,
|
||||||
private val save: Boolean,
|
private val save: Boolean,
|
||||||
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
|
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
|
||||||
|
|
||||||
init {
|
|
||||||
TimeoutHelper.temporarilyDisableTimeout()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Service to prevent background thread kill
|
// TODO Service to prevent background thread kill
|
||||||
override fun run() {
|
override fun run() {
|
||||||
if (save) {
|
if (save) {
|
||||||
@@ -52,8 +45,19 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
|||||||
// Need to call super.run() in child class
|
// Need to call super.run() in child class
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun(isSuccess: Boolean, message: String?) {
|
override fun onFinishRun(result: Result) {
|
||||||
// Need to call super.onFinishRun(isSuccess, message) in child class
|
// Need to call super.onFinishRun(result) in child class
|
||||||
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(context)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
* 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 {
|
callbackRunnable?.apply {
|
||||||
onActionNodeFinish(nodeFinish(isSuccess, message))
|
onActionNodeFinish(nodeFinish(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
displayMessage(context)
|
displayMessage(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onFinishRun(isSuccess, message)
|
super.onFinishRun(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ class AddEntryRunnable constructor(
|
|||||||
database.addEntryTo(mNewEntry, mParent)
|
database.addEntryTo(mNewEntry, mParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
mNewEntry.parent?.let {
|
mNewEntry.parent?.let {
|
||||||
database.removeEntryFrom(mNewEntry, it)
|
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)
|
database.addGroupTo(mNewGroup, mParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
database.removeGroupFrom(mNewGroup, mParent)
|
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
|
package com.kunzisoft.keepass.database.action.node
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback method who return the node(s) modified after an action
|
* 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
|
* - Move : @param oldNode NULL, @param NodeToMove
|
||||||
* - Update : @param oldNode NodeToUpdate, @param NodeUpdated
|
* - 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 class AfterActionNodeFinishRunnable {
|
||||||
abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)
|
abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ class CopyEntryRunnable constructor(
|
|||||||
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, try to delete the copy
|
// If we fail to save, try to delete the copy
|
||||||
try {
|
try {
|
||||||
mEntryCopied?.let {
|
mEntryCopied?.let {
|
||||||
@@ -58,7 +58,7 @@ class CopyEntryRunnable constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return ActionNodeValues(isSuccess, message, mEntryToCopy, mEntryCopied)
|
return ActionNodeValues(result, mEntryToCopy, mEntryCopied)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class DeleteEntryRunnable constructor(
|
|||||||
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
|
||||||
|
|
||||||
private var mParent: GroupVersioned? = null
|
private var mParent: GroupVersioned? = null
|
||||||
private var mRecycle: Boolean = false
|
private var mCanRecycle: Boolean = false
|
||||||
|
|
||||||
|
|
||||||
override fun nodeAction() {
|
override fun nodeAction() {
|
||||||
@@ -41,24 +41,24 @@ class DeleteEntryRunnable constructor(
|
|||||||
mParent?.touch(modified = false, touchParents = true)
|
mParent?.touch(modified = false, touchParents = true)
|
||||||
|
|
||||||
// Remove Entry from parent
|
// Remove Entry from parent
|
||||||
mRecycle = database.canRecycle(mEntryToDelete)
|
mCanRecycle = database.canRecycle(mEntryToDelete)
|
||||||
if (mRecycle) {
|
if (mCanRecycle) {
|
||||||
database.recycle(mEntryToDelete)
|
database.recycle(mEntryToDelete, context.resources)
|
||||||
} else {
|
} else {
|
||||||
database.deleteEntry(mEntryToDelete)
|
database.deleteEntry(mEntryToDelete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
mParent?.let {
|
mParent?.let {
|
||||||
if (mRecycle) {
|
if (mCanRecycle) {
|
||||||
database.undoRecycle(mEntryToDelete, it)
|
database.undoRecycle(mEntryToDelete, it)
|
||||||
} else {
|
} else {
|
||||||
database.undoDeleteEntry(mEntryToDelete, it)
|
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
|
// Remove Group from parent
|
||||||
mRecycle = database.canRecycle(mGroupToDelete)
|
mRecycle = database.canRecycle(mGroupToDelete)
|
||||||
if (mRecycle) {
|
if (mRecycle) {
|
||||||
database.recycle(mGroupToDelete)
|
database.recycle(mGroupToDelete, context.resources)
|
||||||
} else {
|
} else {
|
||||||
database.deleteGroup(mGroupToDelete)
|
database.deleteGroup(mGroupToDelete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
if (mRecycle) {
|
if (mRecycle) {
|
||||||
mParent?.let {
|
mParent?.let {
|
||||||
database.undoRecycle(mGroupToDelete, it)
|
database.undoRecycle(mGroupToDelete, it)
|
||||||
@@ -56,6 +56,6 @@ class DeleteGroupRunnable(context: FragmentActivity,
|
|||||||
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
|
// 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")
|
} ?: Log.e(TAG, "Unable to create a copy of the entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, try to remove in the first place
|
// If we fail to save, try to remove in the first place
|
||||||
try {
|
try {
|
||||||
if (mEntryToMove != null && mOldParent != null)
|
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 {
|
companion object {
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ class MoveGroupRunnable constructor(
|
|||||||
} ?: Log.e(TAG, "Unable to create a copy of the group")
|
} ?: Log.e(TAG, "Unable to create a copy of the group")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, try to move in the first place
|
// If we fail to save, try to move in the first place
|
||||||
try {
|
try {
|
||||||
if (mGroupToMove != null && mOldParent != null)
|
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 {
|
companion object {
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ class UpdateEntryRunnable constructor(
|
|||||||
mOldEntry.updateWith(mNewEntry)
|
mOldEntry.updateWith(mNewEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, back out changes to global structure
|
// If we fail to save, back out changes to global structure
|
||||||
mBackupEntry?.let {
|
mBackupEntry?.let {
|
||||||
mOldEntry.updateWith(it)
|
mOldEntry.updateWith(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ActionNodeValues(isSuccess, message, mOldEntry, mNewEntry)
|
return ActionNodeValues(result, mOldEntry, mNewEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ class UpdateGroupRunnable constructor(
|
|||||||
mOldGroup.updateWith(mNewGroup)
|
mOldGroup.updateWith(mNewGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
|
override fun nodeFinish(result: Result): ActionNodeValues {
|
||||||
if (!isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, back out changes to global structure
|
// If we fail to save, back out changes to global structure
|
||||||
mOldGroup.updateWith(mBackupGroup)
|
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.
|
* 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.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -60,8 +58,6 @@ class Database {
|
|||||||
private var mUri: Uri? = null
|
private var mUri: Uri? = null
|
||||||
private var searchHelper: SearchDbHelper? = null
|
private var searchHelper: SearchDbHelper? = null
|
||||||
var isReadOnly = false
|
var isReadOnly = false
|
||||||
var isPasswordEncodingError = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
val drawFactory = IconDrawableFactory()
|
val drawFactory = IconDrawableFactory()
|
||||||
|
|
||||||
@@ -69,7 +65,7 @@ class Database {
|
|||||||
|
|
||||||
val iconFactory: PwIconFactory
|
val iconFactory: PwIconFactory
|
||||||
get() {
|
get() {
|
||||||
return pwDatabaseV3?.getIconFactory() ?: pwDatabaseV4?.getIconFactory() ?: PwIconFactory()
|
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
val name: String
|
val name: String
|
||||||
@@ -110,13 +106,13 @@ class Database {
|
|||||||
return ArrayList()
|
return ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val kdfEngine: KdfEngine?
|
val kdfEngine: KdfEngine
|
||||||
get() {
|
get() {
|
||||||
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
|
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
|
||||||
}
|
}
|
||||||
|
|
||||||
val numberKeyEncryptionRoundsAsString: String
|
val numberKeyEncryptionRoundsAsString: String
|
||||||
get() = java.lang.Long.toString(numberKeyEncryptionRounds)
|
get() = numberKeyEncryptionRounds.toString()
|
||||||
|
|
||||||
var numberKeyEncryptionRounds: Long
|
var numberKeyEncryptionRounds: Long
|
||||||
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
|
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
|
||||||
@@ -127,7 +123,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val memoryUsageAsString: String
|
val memoryUsageAsString: String
|
||||||
get() = java.lang.Long.toString(memoryUsage)
|
get() = memoryUsage.toString()
|
||||||
|
|
||||||
var memoryUsage: Long
|
var memoryUsage: Long
|
||||||
get() {
|
get() {
|
||||||
@@ -138,7 +134,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val parallelismAsString: String
|
val parallelismAsString: String
|
||||||
get() = Integer.toString(parallelism)
|
get() = parallelism.toString()
|
||||||
|
|
||||||
var parallelism: Int
|
var parallelism: Int
|
||||||
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOW_VALUE
|
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOW_VALUE
|
||||||
@@ -147,7 +143,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var masterKey: ByteArray
|
var masterKey: ByteArray
|
||||||
get() = pwDatabaseV3?.getMasterKey() ?: pwDatabaseV4?.getMasterKey() ?: ByteArray(32)
|
get() = pwDatabaseV3?.masterKey ?: pwDatabaseV4?.masterKey ?: ByteArray(32)
|
||||||
set(masterKey) {
|
set(masterKey) {
|
||||||
pwDatabaseV3?.masterKey = masterKey
|
pwDatabaseV3?.masterKey = masterKey
|
||||||
pwDatabaseV4?.masterKey = masterKey
|
pwDatabaseV4?.masterKey = masterKey
|
||||||
@@ -155,11 +151,11 @@ class Database {
|
|||||||
|
|
||||||
val rootGroup: GroupVersioned?
|
val rootGroup: GroupVersioned?
|
||||||
get() {
|
get() {
|
||||||
pwDatabaseV3?.let {
|
pwDatabaseV3?.rootGroup?.let {
|
||||||
return GroupVersioned(it.rootGroup)
|
return GroupVersioned(it)
|
||||||
}
|
}
|
||||||
pwDatabaseV4?.let {
|
pwDatabaseV4?.rootGroup?.let {
|
||||||
return GroupVersioned(it.rootGroup)
|
return GroupVersioned(it)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -182,24 +178,6 @@ class Database {
|
|||||||
return null
|
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) {
|
private fun setDatabaseV3(pwDatabaseV3: PwDatabaseV3) {
|
||||||
this.pwDatabaseV3 = pwDatabaseV3
|
this.pwDatabaseV3 = pwDatabaseV3
|
||||||
this.pwDatabaseV4 = null
|
this.pwDatabaseV4 = null
|
||||||
@@ -210,17 +188,9 @@ class Database {
|
|||||||
this.pwDatabaseV4 = pwDatabaseV4
|
this.pwDatabaseV4 = pwDatabaseV4
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isKDBExtension(filename: String?): Boolean {
|
private fun dbNameFromUri(databaseUri: Uri): String {
|
||||||
if (filename == null) {
|
val filename = URLUtil.guessFileName(databaseUri.path, null, null)
|
||||||
return false
|
if (filename == null || filename.isEmpty()) {
|
||||||
}
|
|
||||||
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)) {
|
|
||||||
return "KeePass Database"
|
return "KeePass Database"
|
||||||
}
|
}
|
||||||
val lastExtDot = filename.lastIndexOf(".")
|
val lastExtDot = filename.lastIndexOf(".")
|
||||||
@@ -229,8 +199,10 @@ class Database {
|
|||||||
} else filename.substring(0, lastExtDot)
|
} else filename.substring(0, lastExtDot)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUri(mUri: Uri) {
|
fun createData(databaseUri: Uri) {
|
||||||
this.mUri = mUri
|
// Always create a new database with the last version
|
||||||
|
setDatabaseV4(PwDatabaseV4(dbNameFromUri(databaseUri)))
|
||||||
|
this.mUri = databaseUri
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, InvalidDBException::class)
|
@Throws(IOException::class, InvalidDBException::class)
|
||||||
@@ -246,7 +218,7 @@ class Database {
|
|||||||
// Pass Uris as InputStreams
|
// Pass Uris as InputStreams
|
||||||
val inputStream: InputStream?
|
val inputStream: InputStream?
|
||||||
try {
|
try {
|
||||||
inputStream = getUriInputStream(ctx.contentResolver, uri)
|
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KPD", "Database::loadData", e)
|
Log.e("KPD", "Database::loadData", e)
|
||||||
throw ContentFileNotFoundException.getInstance(uri)
|
throw ContentFileNotFoundException.getInstance(uri)
|
||||||
@@ -256,7 +228,7 @@ class Database {
|
|||||||
var keyFileInputStream: InputStream? = null
|
var keyFileInputStream: InputStream? = null
|
||||||
keyfile?.let {
|
keyfile?.let {
|
||||||
try {
|
try {
|
||||||
keyFileInputStream = getUriInputStream(ctx.contentResolver, keyfile)
|
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KPD", "Database::loadData", e)
|
Log.e("KPD", "Database::loadData", e)
|
||||||
throw ContentFileNotFoundException.getInstance(keyfile)
|
throw ContentFileNotFoundException.getInstance(keyfile)
|
||||||
@@ -300,7 +272,6 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isPasswordEncodingError = !(pwDatabaseV3?.validatePasswordEncoding(password) ?: pwDatabaseV4?.validatePasswordEncoding(password) ?: true)
|
|
||||||
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
|
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
|
||||||
loaded = true
|
loaded = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -320,7 +291,7 @@ class Database {
|
|||||||
return searchHelper?.search(this, str, max)
|
return searchHelper?.search(this, str, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchEntry(query: String): Cursor? {
|
fun searchEntries(query: String): Cursor? {
|
||||||
|
|
||||||
var cursorV3: EntryCursorV3? = null
|
var cursorV3: EntryCursorV3? = null
|
||||||
var cursorV4: EntryCursorV4? = null
|
var cursorV4: EntryCursorV4? = null
|
||||||
@@ -348,7 +319,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryFrom(cursor: Cursor): EntryVersioned? {
|
fun getEntryFrom(cursor: Cursor): EntryVersioned? {
|
||||||
val iconFactory = pwDatabaseV3?.getIconFactory() ?: pwDatabaseV4?.getIconFactory() ?: PwIconFactory()
|
val iconFactory = pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
|
||||||
val entry = createEntry()
|
val entry = createEntry()
|
||||||
|
|
||||||
// TODO invert field reference manager
|
// TODO invert field reference manager
|
||||||
@@ -428,21 +399,22 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO Clear database when lock broadcast is receive in backstage
|
// TODO Clear database when lock broadcast is receive in backstage
|
||||||
fun closeAndClear(context: Context) {
|
fun closeAndClear(filesDirectory: File? = null) {
|
||||||
drawFactory.clearCache()
|
drawFactory.clearCache()
|
||||||
// Delete the cache of the database if present
|
// Delete the cache of the database if present
|
||||||
|
pwDatabaseV3?.clearCache()
|
||||||
pwDatabaseV4?.clearCache()
|
pwDatabaseV4?.clearCache()
|
||||||
// In all cases, delete all the files in the temp dir
|
// In all cases, delete all the files in the temp dir
|
||||||
try {
|
try {
|
||||||
FileUtils.cleanDirectory(context.filesDir)
|
FileUtils.cleanDirectory(filesDirectory)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Unable to clear the directory cache.", e)
|
Log.e(TAG, "Unable to clear the directory cache.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pwDatabaseV3 = null
|
||||||
pwDatabaseV4 = null
|
pwDatabaseV4 = null
|
||||||
mUri = null
|
mUri = null
|
||||||
loaded = false
|
loaded = false
|
||||||
isPasswordEncodingError = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVersion(): String {
|
fun getVersion(): String {
|
||||||
@@ -474,9 +446,9 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) {
|
fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) {
|
||||||
pwDatabaseV4?.encryptionAlgorithm = algorithm
|
pwDatabaseV4?.encryptionAlgorithm = algorithm
|
||||||
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
|
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
|
||||||
pwDatabaseV4?.dataCipher = algorithm.dataCipher
|
pwDatabaseV4?.dataCipher = algorithm.dataCipher
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEncryptionAlgorithmName(resources: Resources): String {
|
fun getEncryptionAlgorithmName(resources: Resources): String {
|
||||||
@@ -496,14 +468,13 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyDerivationName(resources: Resources): String {
|
fun getKeyDerivationName(resources: Resources): String {
|
||||||
val kdfEngine = kdfEngine
|
return kdfEngine.getName(resources)
|
||||||
return if (kdfEngine != null) {
|
|
||||||
kdfEngine.getName(resources)
|
|
||||||
} else ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validatePasswordEncoding(key: String): Boolean {
|
fun validatePasswordEncoding(key: String?): Boolean {
|
||||||
return pwDatabaseV3?.validatePasswordEncoding(key) ?: pwDatabaseV4?.validatePasswordEncoding(key) ?: false
|
return pwDatabaseV3?.validatePasswordEncoding(key)
|
||||||
|
?: pwDatabaseV4?.validatePasswordEncoding(key)
|
||||||
|
?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(InvalidKeyFileException::class, IOException::class)
|
@Throws(InvalidKeyFileException::class, IOException::class)
|
||||||
@@ -515,16 +486,12 @@ class Database {
|
|||||||
fun createEntry(): EntryVersioned? {
|
fun createEntry(): EntryVersioned? {
|
||||||
pwDatabaseV3?.let { database ->
|
pwDatabaseV3?.let { database ->
|
||||||
return EntryVersioned(database.createEntry()).apply {
|
return EntryVersioned(database.createEntry()).apply {
|
||||||
database.newEntryId()?.let {
|
nodeId = database.newEntryId()
|
||||||
nodeId = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pwDatabaseV4?.let { database ->
|
pwDatabaseV4?.let { database ->
|
||||||
return EntryVersioned(database.createEntry()).apply {
|
return EntryVersioned(database.createEntry()).apply {
|
||||||
database.newEntryId()?.let {
|
nodeId = database.newEntryId()
|
||||||
nodeId = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,16 +501,12 @@ class Database {
|
|||||||
fun createGroup(): GroupVersioned? {
|
fun createGroup(): GroupVersioned? {
|
||||||
pwDatabaseV3?.let { database ->
|
pwDatabaseV3?.let { database ->
|
||||||
return GroupVersioned(database.createGroup()).apply {
|
return GroupVersioned(database.createGroup()).apply {
|
||||||
database.newGroupId()?.let {
|
setNodeId(database.newGroupId())
|
||||||
setNodeId(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pwDatabaseV4?.let { database ->
|
pwDatabaseV4?.let { database ->
|
||||||
return GroupVersioned(database.createGroup()).apply {
|
return GroupVersioned(database.createGroup()).apply {
|
||||||
database.newGroupId()?.let {
|
setNodeId(database.newGroupId())
|
||||||
setNodeId(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,26 +534,42 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addEntryTo(entry: EntryVersioned, parent: GroupVersioned) {
|
fun addEntryTo(entry: EntryVersioned, parent: GroupVersioned) {
|
||||||
pwDatabaseV3?.addEntryTo(entry.pwEntryV3, parent.pwGroupV3)
|
entry.pwEntryV3?.let { entryV3 ->
|
||||||
pwDatabaseV4?.addEntryTo(entry.pwEntryV4, parent.pwGroupV4)
|
pwDatabaseV3?.addEntryTo(entryV3, parent.pwGroupV3)
|
||||||
|
}
|
||||||
|
entry.pwEntryV4?.let { entryV4 ->
|
||||||
|
pwDatabaseV4?.addEntryTo(entryV4, parent.pwGroupV4)
|
||||||
|
}
|
||||||
entry.afterAssignNewParent()
|
entry.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFrom(entry: EntryVersioned, parent: GroupVersioned) {
|
fun removeEntryFrom(entry: EntryVersioned, parent: GroupVersioned) {
|
||||||
pwDatabaseV3?.removeEntryFrom(entry.pwEntryV3, parent.pwGroupV3)
|
entry.pwEntryV3?.let { entryV3 ->
|
||||||
pwDatabaseV4?.removeEntryFrom(entry.pwEntryV4, parent.pwGroupV4)
|
pwDatabaseV3?.removeEntryFrom(entryV3, parent.pwGroupV3)
|
||||||
|
}
|
||||||
|
entry.pwEntryV4?.let { entryV4 ->
|
||||||
|
pwDatabaseV4?.removeEntryFrom(entryV4, parent.pwGroupV4)
|
||||||
|
}
|
||||||
entry.afterAssignNewParent()
|
entry.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addGroupTo(group: GroupVersioned, parent: GroupVersioned) {
|
fun addGroupTo(group: GroupVersioned, parent: GroupVersioned) {
|
||||||
pwDatabaseV3?.addGroupTo(group.pwGroupV3, parent.pwGroupV3)
|
group.pwGroupV3?.let { groupV3 ->
|
||||||
pwDatabaseV4?.addGroupTo(group.pwGroupV4, parent.pwGroupV4)
|
pwDatabaseV3?.addGroupTo(groupV3, parent.pwGroupV3)
|
||||||
|
}
|
||||||
|
group.pwGroupV4?.let { groupV4 ->
|
||||||
|
pwDatabaseV4?.addGroupTo(groupV4, parent.pwGroupV4)
|
||||||
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(group: GroupVersioned, parent: GroupVersioned) {
|
fun removeGroupFrom(group: GroupVersioned, parent: GroupVersioned) {
|
||||||
pwDatabaseV3?.removeGroupFrom(group.pwGroupV3, parent.pwGroupV3)
|
group.pwGroupV3?.let { groupV3 ->
|
||||||
pwDatabaseV4?.removeGroupFrom(group.pwGroupV4, parent.pwGroupV4)
|
pwDatabaseV3?.removeGroupFrom(groupV3, parent.pwGroupV3)
|
||||||
|
}
|
||||||
|
group.pwGroupV4?.let { groupV4 ->
|
||||||
|
pwDatabaseV4?.removeGroupFrom(groupV4, parent.pwGroupV4)
|
||||||
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,37 +626,65 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun undoDeleteEntry(entry: EntryVersioned, parent: GroupVersioned) {
|
fun undoDeleteEntry(entry: EntryVersioned, parent: GroupVersioned) {
|
||||||
pwDatabaseV3?.undoDeleteEntryFrom(entry.pwEntryV3, parent.pwGroupV3)
|
entry.pwEntryV3?.let { entryV3 ->
|
||||||
pwDatabaseV4?.undoDeleteEntryFrom(entry.pwEntryV4, parent.pwGroupV4)
|
pwDatabaseV3?.undoDeleteEntryFrom(entryV3, parent.pwGroupV3)
|
||||||
|
}
|
||||||
|
entry.pwEntryV4?.let { entryV4 ->
|
||||||
|
pwDatabaseV4?.undoDeleteEntryFrom(entryV4, parent.pwGroupV4)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun undoDeleteGroup(group: GroupVersioned, parent: GroupVersioned) {
|
fun undoDeleteGroup(group: GroupVersioned, parent: GroupVersioned) {
|
||||||
pwDatabaseV3?.undoDeleteGroupFrom(group.pwGroupV3, parent.pwGroupV3)
|
group.pwGroupV3?.let { groupV3 ->
|
||||||
pwDatabaseV4?.undoDeleteGroupFrom(group.pwGroupV4, parent.pwGroupV4)
|
pwDatabaseV3?.undoDeleteGroupFrom(groupV3, parent.pwGroupV3)
|
||||||
|
}
|
||||||
|
group.pwGroupV4?.let { groupV4 ->
|
||||||
|
pwDatabaseV4?.undoDeleteGroupFrom(groupV4, parent.pwGroupV4)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canRecycle(entry: EntryVersioned): Boolean {
|
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 {
|
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) {
|
fun recycle(entry: EntryVersioned, resources: Resources) {
|
||||||
pwDatabaseV4?.recycle(entry.pwEntryV4)
|
entry.pwEntryV4?.let {
|
||||||
|
pwDatabaseV4?.recycle(it, resources)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: GroupVersioned) {
|
fun recycle(group: GroupVersioned, resources: Resources) {
|
||||||
pwDatabaseV4?.recycle(group.pwGroupV4)
|
group.pwGroupV4?.let {
|
||||||
|
pwDatabaseV4?.recycle(it, resources)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun undoRecycle(entry: EntryVersioned, parent: GroupVersioned) {
|
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) {
|
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) {
|
fun startManageEntry(entry: EntryVersioned) {
|
||||||
|
|||||||
@@ -304,4 +304,22 @@ class GroupVersioned : NodeVersioned, PwGroupInterface<GroupVersioned, EntryVers
|
|||||||
fun containsCustomData(): Boolean {
|
fun containsCustomData(): Boolean {
|
||||||
return pwGroupV4?.containsCustomData() ?: false
|
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.
|
* 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.ProtectedBinary
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.utils.MemUtil
|
import com.kunzisoft.keepass.utils.MemUtil
|
||||||
import com.kunzisoft.keepass.utils.SprEngineV4
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
|
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 decodeRef
|
||||||
* @param key
|
* @param key
|
||||||
* @return
|
* @return
|
||||||
@@ -175,7 +174,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
|
|||||||
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
|
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
|
||||||
val text = fields.getProtectedStringValue(key)
|
val text = fields.getProtectedStringValue(key)
|
||||||
return if (decodeRef) {
|
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
|
} 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.
|
* 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.
|
* 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.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
@@ -20,14 +20,8 @@
|
|||||||
package com.kunzisoft.keepass.database.exception
|
package com.kunzisoft.keepass.database.exception
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by bpellin on 3/14/16.
|
|
||||||
*/
|
|
||||||
class ContentFileNotFoundException : FileNotFoundException() {
|
class ContentFileNotFoundException : FileNotFoundException() {
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(uri: Uri?): FileNotFoundException {
|
fun getInstance(uri: Uri?): FileNotFoundException {
|
||||||
@@ -36,11 +30,11 @@ class ContentFileNotFoundException : FileNotFoundException() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val scheme = uri.scheme
|
val scheme = uri.scheme
|
||||||
|
return if (scheme != null
|
||||||
return if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("content", ignoreCase = true)) {
|
&& scheme.isNotEmpty()
|
||||||
|
&& scheme.equals("content", ignoreCase = true)) {
|
||||||
ContentFileNotFoundException()
|
ContentFileNotFoundException()
|
||||||
} else FileNotFoundException()
|
} 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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