Merge branch 'feature/Refactor_Kotlin' of github.com:Kunzisoft/KeePassDX into feature/Refactor_Kotlin

This commit is contained in:
J-Jamet
2019-07-27 09:35:42 +02:00
279 changed files with 8159 additions and 9096 deletions

View File

@@ -2,6 +2,9 @@ KeepassDX (2.5.0.0beta19)
* Add lock button always visible
* Refactor connection workflow
* Better Magikeyboard connection
* Kotlinized code
* Fix Recycle Bin
* Fix small bugs
KeepassDX (2.5.0.0beta18)
* New recent databases views

View File

@@ -63,7 +63,7 @@ Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
## License
Copyright (c) 2017 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePass DX.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -19,18 +19,13 @@
*/
package com.kunzisoft.keepass.tests;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Environment;
import com.kunzisoft.keepass.utils.EmptyUtils;
import com.kunzisoft.keepass.utils.UriUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
public class TestUtil {
private static final File sdcard = Environment.getExternalStorageDirectory();

View File

@@ -68,11 +68,11 @@ public class AESTest extends TestCase {
mRand.nextBytes(ivArray);
IvParameterSpec iv = new IvParameterSpec(ivArray);
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true);
Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true);
android.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outAndroid = android.doFinal(input, 0, dataSize);
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
Cipher nat = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding");
nat.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outNative = nat.doFinal(input, 0, dataSize);

View File

@@ -56,7 +56,7 @@ public class CipherTest extends TestCase {
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
@@ -78,7 +78,7 @@ public class CipherTest extends TestCase {
rand.nextBytes(iv);
rand.nextBytes(plaintext);
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);

View File

@@ -23,16 +23,10 @@ import android.content.Context;
import android.content.res.AssetManager;
import android.test.AndroidTestCase;
import com.kunzisoft.keepass.database.element.PwDatabase;
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
import com.kunzisoft.keepass.database.element.PwEntryV4;
import com.kunzisoft.keepass.utils.SprEngineV4;
import com.kunzisoft.keepass.utils.Types;
import com.kunzisoft.keepass.database.element.SprEngineV4;
import java.io.InputStream;
import java.util.UUID;
import biz.source_code.base64Coder.Base64Coder;
public class SprEngineTest extends AndroidTestCase {
private PwDatabaseV4 db;
@@ -73,14 +67,4 @@ public class SprEngineTest extends AndroidTestCase {
*/
}
private UUID decodeUUID(String encoded) {
if (encoded == null || encoded.length() == 0 ) {
return PwDatabase.UUID_ZERO;
}
byte[] buf = Base64Coder.decode(encoded);
return Types.bytestoUUID(buf);
}
}

View File

@@ -142,6 +142,10 @@
</activity>
<service
android:name="com.kunzisoft.keepass.database.action.DatabaseTaskNotificationService"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Keyboard -->
<receiver
android:name="com.kunzisoft.keepass.magikeyboard.receiver.NotificationDeleteBroadcastReceiver"

View File

@@ -42,9 +42,8 @@ class AboutActivity : StylishActivity() {
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.title = getString(R.string.menu_about)
setSupportActionBar(toolbar)
assert(supportActionBar != null)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
var version: String
var build: String

View File

@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwNodeId
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.notifications.NotificationEntryCopyManager
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
@@ -140,7 +141,7 @@ class EntryActivity : LockingHideActivity() {
val database = App.currentDatabase
database.startManageEntry(entry)
// Assign title icon
database.drawFactory.assignDatabaseIconTo(this, titleIconView, entry.icon, iconColor)
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
// Assign title text
titleView?.text = entry.getVisualTitle()

View File

@@ -35,7 +35,6 @@ import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment.Companion.KEY_ICON_STANDARD
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
import com.kunzisoft.keepass.view.EntryEditCustomField
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
@@ -45,11 +44,15 @@ import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.utils.applyFontVisibility
import com.kunzisoft.keepass.view.EntryEditCustomField
class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPickerListener, GeneratePasswordDialogFragment.GeneratePasswordListener {
@@ -140,7 +143,9 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
mEntry = mDatabase?.createEntry()
mParent = mDatabase?.getGroupById(it)
// Add the default icon
mDatabase?.drawFactory?.assignDefaultDatabaseIconTo(this, entryIconView, iconColor)
mDatabase?.drawFactory?.let { iconFactory ->
entryIconView?.assignDefaultDatabaseIcon(iconFactory, iconColor)
}
}
// Close the activity if entry or parent can't be retrieve
@@ -252,7 +257,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
var actionRunnable: ActionRunnable? = null
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
if (actionNodeValues.success)
if (actionNodeValues.result.isSuccess)
finish()
}
}
@@ -306,7 +311,7 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
for (i in 0 until it.childCount) {
val entryEditCustomField = it.getChildAt(i) as EntryEditCustomField
val key = entryEditCustomField.label
if (key == null || key.isEmpty()) {
if (key.isEmpty()) {
validationErrorMessageId = R.string.error_string_key
return false
}
@@ -392,12 +397,8 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
}
private fun assignIconView() {
mEntry?.icon?.let {
mDatabase?.drawFactory?.assignDatabaseIconTo(
this,
entryIconView,
it,
iconColor)
if (mDatabase?.drawFactory != null && mEntry?.icon != null) {
entryIconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, mEntry?.icon!!, iconColor)
}
}
@@ -418,10 +419,10 @@ class EntryEditActivity : LockingHideActivity(), IconPickerDialogFragment.IconPi
val visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this)
if (visibilityFontActivated) {
Util.applyFontVisibilityTo(this, entryUserNameView)
Util.applyFontVisibilityTo(this, entryPasswordView)
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView)
Util.applyFontVisibilityTo(this, entryCommentView)
entryUserNameView?.applyFontVisibility()
entryPasswordView?.applyFontVisibility()
entryConfirmationPasswordView?.applyFontVisibility()
entryCommentView?.applyFontVisibility()
}
if (entry.allowExtraFields()) {

View File

@@ -49,6 +49,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.*
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -59,7 +60,6 @@ import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import net.cachapa.expandablelayout.ExpandableLayout
@@ -170,13 +170,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
if (fileName!!.isNotEmpty()) {
val dbUri = UriUtil.parseDefaultFile(fileName)
if (fileName != null && fileName.isNotEmpty()) {
val dbUri = UriUtil.parseUriFile(fileName)
var scheme: String? = null
if (dbUri != null)
scheme = dbUri.scheme
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("file", ignoreCase = true)) {
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
val path = dbUri!!.path
val db = File(path!!)
@@ -404,23 +404,21 @@ class FileDatabaseSelectActivity : StylishActivity(),
keyFileChecked: Boolean, keyFile: Uri?) {
try {
mDatabaseFileUri?.path?.let { databaseFilename ->
// Create the new database and start prof
UriUtil.parseUriFile(mDatabaseFileUri)?.let { databaseUri ->
// Create the new database
ProgressDialogThread(this@FileDatabaseSelectActivity,
{
CreateDatabaseRunnable(databaseFilename) { database ->
// TODO store database created
AssignPasswordInDatabaseRunnable(
this@FileDatabaseSelectActivity,
database,
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
databaseUri,
App.currentDatabase,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile,
true, // TODO get readonly
LaunchGroupActivityFinish(UriUtil.parseDefaultFile(databaseFilename))
LaunchGroupActivityFinish(databaseUri)
)
}
},
R.string.progress_create)
.start()
@@ -432,7 +430,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
// TODO remove
e.printStackTrace()
}
}
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
@@ -441,9 +438,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
finishRun(true, null)
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {
override fun onFinishRun(result: Result) {
runOnUiThread {
if (isSuccess) {
if (result.isSuccess) {
// Add database to recent files
mFileDatabaseHistory?.addDatabaseUri(fileURI)
mAdapterDatabaseHistory?.notifyDataSetChanged()

View File

@@ -55,10 +55,10 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.action.AssignPasswordInDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.KeyboardEntryNotificationService
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.magikeyboard.MagikIME
@@ -193,16 +193,16 @@ class GroupActivity : LockingActivity(),
.commit()
// Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener {
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
GroupEditDialogFragment.build()
.show(supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP)
}
addNodeButtonView?.setAddEntryClickListener {
})
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
mCurrentGroup?.let { currentGroup ->
EntryEditActivity.launch(this@GroupActivity, currentGroup)
}
}
})
// Search suggestion
mDatabase?.let { database ->
@@ -348,7 +348,8 @@ class GroupActivity : LockingActivity(),
// Assign the group icon depending of IconPack or custom icon
iconView?.visibility = View.VISIBLE
mCurrentGroup?.let {
mDatabase?.drawFactory?.assignDatabaseIconTo(this, iconView, it.icon, mIconColor)
if (mDatabase?.drawFactory != null)
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor)
if (toolbar != null) {
if (mCurrentGroup?.containsParent() == true)
@@ -408,7 +409,7 @@ class GroupActivity : LockingActivity(),
EntryActivity.launch(this@GroupActivity, entry, readOnly)
},
{
MagikIME.setEntryKey(getEntry(entry))
MagikIME.entryKey = getEntry(entry)
// Show the notification if allowed in Preferences
if (PreferencesUtil.enableKeyboardNotificationEntry(this@GroupActivity)) {
startService(Intent(
@@ -442,7 +443,7 @@ class GroupActivity : LockingActivity(),
if (entry.containsCustomFields()) {
entry.fields
.doActionToAllCustomProtectedField { key, value ->
entryModel.addCustomField(
entryModel.customFields.add(
Field(key, value.toString()))
}
}
@@ -603,12 +604,6 @@ class GroupActivity : LockingActivity(),
mSearchSuggestionAdapter?.reInit(this)
}
override fun onStop() {
super.onStop()
// Hide button
addNodeButtonView?.hideButton()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
@@ -661,10 +656,10 @@ class GroupActivity : LockingActivity(),
// If no node, show education to add new one
if (mListNodesFragment != null
&& mListNodesFragment!!.isEmpty
&& addNodeButtonView != null
&& addNodeButtonView?.addButtonView != null
&& addNodeButtonView!!.isEnable
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
addNodeButtonView!!,
addNodeButtonView?.addButtonView!!,
{
addNodeButtonView?.openButtonIfClose()
},
@@ -829,7 +824,7 @@ class GroupActivity : LockingActivity(),
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.success) {
if (actionNodeValues.result.isSuccess) {
if (actionNodeValues.newNode != null)
mListNodesFragment?.addNode(actionNodeValues.newNode)
}
@@ -840,7 +835,7 @@ class GroupActivity : LockingActivity(),
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.success) {
if (actionNodeValues.result.isSuccess) {
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
}
@@ -851,22 +846,20 @@ class GroupActivity : LockingActivity(),
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
runOnUiThread {
if (actionNodeValues.success) {
if (actionNodeValues.oldNode != null)
mListNodesFragment?.removeNode(actionNodeValues.oldNode)
if (actionNodeValues.result.isSuccess) {
actionNodeValues.oldNode?.let { oldNode ->
oldNode.parent?.let { parent ->
mListNodesFragment?.removeNode(oldNode)
// TODO Move trash view
// Add trash in views list if it doesn't exists
val database = App.currentDatabase
if (database.isRecycleBinAvailable && database.isRecycleBinEnabled) {
if (database.isRecycleBinEnabled) {
val recycleBin = database.recycleBin
// Add trash if it doesn't exists
if (parent == recycleBin
&& mCurrentGroup != null
if (mCurrentGroup != null && recycleBin != null
&& mCurrentGroup!!.parent == null
&& mCurrentGroup != recycleBin) {
mListNodesFragment?.addNode(parent)
}
mListNodesFragment?.addNode(recycleBin)
}
}
}
@@ -877,7 +870,7 @@ class GroupActivity : LockingActivity(),
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
name: String?,
iconId: PwIcon?) {
icon: PwIcon?) {
// Do nothing here
}
@@ -913,11 +906,15 @@ class GroupActivity : LockingActivity(),
true)
}
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword!!)) {
if (database.validatePasswordEncoding(masterPassword)) {
progressDialogThread.start()
} else {
PasswordEncodingDialogHelper()
.show(this, DialogInterface.OnClickListener{ _, _ -> progressDialogThread.start() })
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
progressDialogThread.start()
}
show(supportFragmentManager, "passwordEncodingTag")
}
}
}
}
@@ -940,7 +937,7 @@ class GroupActivity : LockingActivity(),
}
// Not directly get the entry from intent data but from database
// Is refresh from onResume()
mListNodesFragment?.rebuildList()
}
@SuppressLint("RestrictedApi")
@@ -976,7 +973,7 @@ class GroupActivity : LockingActivity(),
// Else lock if needed
else {
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
App.currentDatabase.closeAndClear(applicationContext)
App.currentDatabase.closeAndClear(applicationContext.filesDir)
super.onBackPressed()
} else {
moveTaskToBack(true)

View File

@@ -94,7 +94,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
mAdapter = NodeAdapter(getContextThemed(), currentActivity.menuInflater)
mAdapter = NodeAdapter(contextThemed, currentActivity.menuInflater)
mAdapter?.apply {
setReadOnly(readOnly)
setIsASearchResult(isASearchResult)
@@ -115,7 +115,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
super.onCreateView(inflater, container, savedInstanceState)
// To apply theme
val rootView = inflater.cloneInContext(getContextThemed())
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.list_nodes_fragment, container, false)
listView = rootView.findViewById(R.id.nodes_list)
notFoundView = rootView.findViewById(R.id.not_found_container)
@@ -129,14 +129,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
})
}
rebuildList()
return rootView
}
override fun onResume() {
super.onResume()
rebuildList()
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
listView?.visibility = View.GONE
@@ -186,6 +186,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
when (item?.itemId) {
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment
/*
@@ -206,6 +207,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
//}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
return true
}

View File

@@ -43,7 +43,7 @@ import android.view.MenuItem
import android.view.View
import android.widget.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogHelper
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.*
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.stylish.StylishActivity
@@ -59,7 +59,6 @@ import com.kunzisoft.keepass.fingerprint.FingerPrintHelper
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import permissions.dispatcher.*
@@ -265,8 +264,8 @@ class PasswordActivity : StylishActivity(),
// Retrieve settings for default database
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
if (mDatabaseFileUri != null
&& !EmptyUtils.isNullOrEmpty(mDatabaseFileUri!!.path)
&& UriUtil.equalsDefaultfile(mDatabaseFileUri, defaultFilename)) {
&& mDatabaseFileUri!!.path != null && mDatabaseFileUri!!.path!!.isNotEmpty()
&& mDatabaseFileUri == UriUtil.parseUriFile(defaultFilename)) {
checkboxDefaultDatabaseView?.isChecked = true
}
@@ -526,9 +525,9 @@ class PasswordActivity : StylishActivity(),
setFingerPrintView(R.string.encrypted_value_stored)
}
override fun handleDecryptedResult(passwordValue: String) {
override fun handleDecryptedResult(value: String) {
// Load database directly
verifyKeyFileViewsAndLoadDatabase(passwordValue)
verifyKeyFileViewsAndLoadDatabase(value)
}
@RequiresApi(api = Build.VERSION_CODES.M)
@@ -563,8 +562,8 @@ class PasswordActivity : StylishActivity(),
private fun verifyAllViewsAndLoadDatabase() {
verifyCheckboxesAndLoadDatabase(
passwordView?.text.toString(),
UriUtil.parseDefaultFile(keyFileView?.text.toString()))
passwordView?.text?.toString(),
UriUtil.parseUriFile(keyFileView?.text?.toString()))
}
private fun verifyCheckboxesAndLoadDatabase(password: String?, keyFile: Uri?) {
@@ -580,8 +579,8 @@ class PasswordActivity : StylishActivity(),
}
private fun verifyKeyFileViewsAndLoadDatabase(password: String) {
val key = keyFileView?.text.toString()
var keyUri = UriUtil.parseDefaultFile(key)
val key = keyFileView?.text?.toString()
var keyUri = UriUtil.parseUriFile(key)
if (checkboxKeyFileView?.isChecked != true) {
keyUri = null
}
@@ -591,20 +590,20 @@ class PasswordActivity : StylishActivity(),
private fun loadDatabase(password: String?, keyFile: Uri?) {
// Clear before we load
val database = App.currentDatabase
database.closeAndClear(applicationContext)
database.closeAndClear(applicationContext.filesDir)
mDatabaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
WeakReference(this@PasswordActivity.applicationContext),
WeakReference(this@PasswordActivity),
database,
databaseUri,
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database))
AfterLoadingDatabase(database, password))
},
R.string.loading_database).start()
}
@@ -613,9 +612,11 @@ class PasswordActivity : StylishActivity(),
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase internal constructor(var database: Database) : ActionRunnable() {
private inner class AfterLoadingDatabase internal constructor(var database: Database,
val password: String?)
: ActionRunnable() {
override fun onFinishRun(isSuccess: Boolean, message: String?) {
override fun onFinishRun(result: Result) {
runOnUiThread {
// Recheck fingerprint if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -623,15 +624,20 @@ class PasswordActivity : StylishActivity(),
reInitWithFingerprintMode()
}
if (database.isPasswordEncodingError) {
val dialog = PasswordEncodingDialogHelper()
dialog.show(this@PasswordActivity,
DialogInterface.OnClickListener { _, _ -> launchGroupActivity() })
} else if (isSuccess) {
if (result.isSuccess) {
if (database.validatePasswordEncoding(password)) {
launchGroupActivity()
} else {
if (message != null && message.isNotEmpty()) {
Toast.makeText(this@PasswordActivity, message, Toast.LENGTH_LONG).show()
PasswordEncodingDialogFragment().apply {
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
launchGroupActivity()
}
show(supportFragmentManager, "passwordEncodingTag")
}
}
} else {
if (result.message != null && result.message!!.isNotEmpty()) {
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
}
}
}
@@ -762,7 +768,7 @@ class PasswordActivity : StylishActivity(),
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
setEmptyViews()
App.currentDatabase.closeAndClear(applicationContext)
App.currentDatabase.closeAndClear(applicationContext.filesDir)
}
}
}
@@ -814,14 +820,13 @@ class PasswordActivity : StylishActivity(),
@Throws(FileNotFoundException::class)
private fun verifyFileNameUriFromLaunch(fileName: String) {
if (EmptyUtils.isNullOrEmpty(fileName)) {
if (fileName.isEmpty()) {
throw FileNotFoundException()
}
val uri = UriUtil.parseDefaultFile(fileName)
val scheme = uri.scheme
if (!EmptyUtils.isNullOrEmpty(scheme) && scheme.equals("file", ignoreCase = true)) {
val uri = UriUtil.parseUriFile(fileName)
val scheme = uri?.scheme
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
val dbFile = File(uri.path!!)
if (!dbFile.exists()) {
throw FileNotFoundException()

View File

@@ -35,7 +35,6 @@ import android.widget.TextView
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.UriUtil
class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -178,13 +177,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private fun verifyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked
&& keyFileView != null) {
val keyFile = UriUtil.parseDefaultFile(keyFileView!!.text.toString())
&& keyFileCheckBox!!.isChecked) {
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
mKeyFile = keyFile
// Verify that a keyfile is set
if (EmptyUtils.isNullOrEmpty(keyFile)) {
if (keyFile == null || keyFile.toString().isEmpty()) {
error = true
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
}
@@ -229,11 +227,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
uri?.let { currentUri ->
UriUtil.parseDefaultFile(currentUri.toString())?.let { pathString ->
UriUtil.parseUriFile(uri)?.let { pathUri ->
keyFileCheckBox?.isChecked = true
keyFileView?.text = pathString.toString()
}
keyFileView?.text = pathUri.toString()
}
}
}

View File

@@ -39,13 +39,13 @@ class BrowserDialogFragment : DialogFragment() {
val market = root.findViewById<Button>(R.id.install_market)
market.setOnClickListener {
Util.gotoUrl(context, R.string.filemanager_play_store)
Util.gotoUrl(context!!, R.string.filemanager_play_store)
dismiss()
}
val web = root.findViewById<Button>(R.id.install_web)
web.setOnClickListener {
Util.gotoUrl(context, R.string.filemanager_f_droid)
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
dismiss()
}

View File

@@ -142,7 +142,7 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
// Spinner Drop down elements
val fileTypes = resources.getStringArray(R.array.file_types)
val dataAdapter = ArrayAdapter(activity!!, android.R.layout.simple_spinner_item, fileTypes)
val dataAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, fileTypes)
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = dataAdapter
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
@@ -158,7 +158,7 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
val dialog = builder.create()
dialog.setOnShowListener { dialog1 ->
dialog.setOnShowListener { _ ->
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
positiveButton?.setOnClickListener { _ ->
@@ -181,11 +181,13 @@ class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedLis
}
private fun buildPath(): Uri? {
if (folderPathView != null && mDatabaseFileExtension != null) {
if (folderPathView != null && fileNameView != null && mDatabaseFileExtension != null) {
var path = Uri.Builder().path(folderPathView!!.text.toString())
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
.build()
path = UriUtil.translate(context, path)
context?.let { context ->
path = UriUtil.translateUri(context, path)
}
return path
}
return null

View File

@@ -30,6 +30,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.Util
import com.kunzisoft.keepass.utils.applyFontVisibility
class GeneratePasswordDialogFragment : DialogFragment() {
@@ -66,7 +67,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
root = inflater.inflate(R.layout.generate_password, null)
passwordView = root?.findViewById(R.id.password)
Util.applyFontVisibilityTo(context, passwordView)
passwordView?.applyFontVisibility()
lengthTextView = root?.findViewById(R.id.length)
@@ -92,7 +93,10 @@ class GeneratePasswordDialogFragment : DialogFragment() {
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
context?.let { context ->
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
}
root?.findViewById<Button>(R.id.generate_password_button)
?.setOnClickListener { fillPassword() }
@@ -131,8 +135,9 @@ class GeneratePasswordDialogFragment : DialogFragment() {
bracketsBox?.isChecked = false
extendedBox?.isChecked = false
val defaultPasswordChars = PreferencesUtil.getDefaultPasswordCharacters(context)
for (passwordChar in defaultPasswordChars) {
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
for (passwordChar in charSet) {
when (passwordChar) {
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
@@ -146,6 +151,8 @@ class GeneratePasswordDialogFragment : DialogFragment() {
}
}
}
}
}
private fun fillPassword() {
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
@@ -156,7 +163,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
val generator = PasswordGenerator(activity)
val generator = PasswordGenerator(resources)
password = generator.generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,

View File

@@ -34,6 +34,7 @@ import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.GroupVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.icons.assignDatabaseIcon
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
@@ -45,7 +46,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
private var nameGroup: String? = null
private var iconGroup: PwIcon? = null
private var iconButton: ImageView? = null
private var iconButtonView: ImageView? = null
private var iconColor: Int = 0
enum class EditGroupDialogAction {
@@ -76,7 +77,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.group_edit, null)
val nameField = root?.findViewById<TextView>(R.id.group_edit_name)
iconButton = root?.findViewById(R.id.group_edit_icon_button)
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
// Retrieve the textColor to tint the icon
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
@@ -133,7 +134,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
this@GroupEditDialogFragment.dialog.cancel()
}
iconButton?.setOnClickListener { _ ->
iconButtonView?.setOnClickListener { _ ->
fragmentManager?.let {
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
}
@@ -145,12 +146,9 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
}
private fun assignIconView() {
mDatabase?.drawFactory
?.assignDatabaseIconTo(
context,
iconButton,
iconGroup,
iconColor)
if (mDatabase?.drawFactory != null && iconGroup != null) {
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
}
}
override fun iconPicked(bundle: Bundle) {

View File

@@ -61,7 +61,7 @@ class IconPickerDialogFragment : DialogFragment() {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
iconPack = IconPackChooser.getSelectedIconPack(context)
iconPack = IconPackChooser.getSelectedIconPack(context!!)
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout

View File

@@ -47,9 +47,9 @@ class KeyboardExplanationDialogFragment : DialogFragment() {
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
if (BuildConfig.CLOSED_STORE) {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context, R.string.keyboard_switcher_play_store) }
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
} else {
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context, R.string.keyboard_switcher_f_droid) }
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
}
builder.setView(rootView)

View File

@@ -20,25 +20,27 @@
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.content.Context
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.support.v4.app.DialogFragment
import com.kunzisoft.keepass.R
class PasswordEncodingDialogHelper {
private var dialog: AlertDialog? = null
class PasswordEncodingDialogFragment : DialogFragment() {
@JvmOverloads
fun show(ctx: Context, onclick: DialogInterface.OnClickListener, showCancel: Boolean = false) {
val builder = AlertDialog.Builder(ctx)
builder.setMessage(R.string.warning_password_encoding).setTitle(R.string.warning)
builder.setPositiveButton(android.R.string.ok, onclick)
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
if (showCancel) {
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
dialog = builder.create()
dialog?.show()
}
}

View File

@@ -49,7 +49,7 @@ class ProFeatureDialogFragment : DialogFragment() {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context, R.string.app_pro_url)
Util.gotoUrl(context!!, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
@@ -59,7 +59,7 @@ class ProFeatureDialogFragment : DialogFragment() {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context, R.string.contribution_url)
Util.gotoUrl(context!!, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}

View File

@@ -40,7 +40,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity!!)
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (BuildConfig.CLOSED_STORE) {
@@ -49,32 +49,32 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
builder.setPositiveButton(android.R.string.ok) { dialog, id -> dismiss() }
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
builder.setPositiveButton(R.string.download) { dialog, id ->
builder.setPositiveButton(R.string.download) { _, _ ->
try {
Util.gotoUrl(context, R.string.app_pro_url)
Util.gotoUrl(context!!, R.string.app_pro_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, id -> dismiss() }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
} else {
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
builder.setPositiveButton(R.string.contribute) { dialog, id ->
builder.setPositiveButton(R.string.contribute) { _, _ ->
try {
Util.gotoUrl(context, R.string.contribution_url)
Util.gotoUrl(context!!, R.string.contribution_url)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, id -> dismiss() }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it

View File

@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.activities.helpers
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.support.v4.app.Fragment
@@ -30,7 +32,6 @@ import android.util.Log
import android.view.View
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.fileselect.StorageAF
import com.kunzisoft.keepass.utils.Interaction
import com.kunzisoft.keepass.utils.UriUtil
class KeyFileHelper {
@@ -55,7 +56,7 @@ class KeyFileHelper {
override fun onClick(v: View) {
try {
if (StorageAF.useStorageFramework(activity)) {
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
openActivityWithActionOpenDocument()
} else {
openActivityWithActionGetContent()
@@ -67,7 +68,6 @@ class KeyFileHelper {
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
showBrowserDialog()
}
}
}
@@ -105,7 +105,7 @@ class KeyFileHelper {
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
var showBrowser = false
try {
if (Interaction.isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
// Get file path parent if possible
if (dataUri != null
@@ -130,6 +130,26 @@ class KeyFileHelper {
return showBrowser
}
/**
* Indicates whether the specified action can be used as an intent. This
* method queries the package manager for installed packages that can
* respond to an intent with the specified action. If no suitable package is
* found, this method returns false.
*
* @param context The application's environment.
* @param action The Intent action to check for availability.
*
* @return True if an Intent with the specified action can be sent and
* responded to, false otherwise.
*/
private fun isIntentAvailable(context: Context, action: String): Boolean {
val packageManager = context.packageManager
val intent = Intent(action)
val list = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
return list.size > 0
}
/**
* Show Browser dialog to select file picker app
*/
@@ -162,7 +182,7 @@ class KeyFileHelper {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parseDefaultFile(filename)
keyUri = UriUtil.parseUriFile(filename)
}
keyFileCallback?.invoke(keyUri)
}
@@ -173,7 +193,7 @@ class KeyFileHelper {
if (data != null) {
var uri = data.data
if (uri != null) {
if (StorageAF.useStorageFramework(activity)) {
if (StorageAF.useStorageFramework(activity!!)) {
try {
// try to persist read and write permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -185,10 +205,9 @@ class KeyFileHelper {
} catch (e: Exception) {
// nop
}
}
if (requestCode == GET_CONTENT) {
uri = UriUtil.translate(activity, uri)
uri = UriUtil.translateUri(activity!!, uri)
}
keyFileCallback?.invoke(uri)
}

View File

@@ -58,8 +58,8 @@ class UriIntentInitTask(private val weakContext: WeakReference<Context>,
}
} else {
databaseUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_FILENAME))
keyFileUri = UriUtil.parseDefaultFile(intent.getStringExtra(KEY_KEYFILE))
databaseUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_FILENAME))
keyFileUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_KEYFILE))
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
keyFileUri = getKeyFileUri(databaseUri)

View File

@@ -71,13 +71,12 @@ abstract class LockingActivity : StylishActivity() {
}
if (timeoutEnable) {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this)) {
lockReceiver = LockReceiver()
val intentFilter = IntentFilter()
intentFilter.addAction(Intent.ACTION_SCREEN_OFF)
intentFilter.addAction(LOCK_ACTION)
registerReceiver(lockReceiver, IntentFilter(intentFilter))
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(LOCK_ACTION)
}
registerReceiver(lockReceiver, IntentFilter(intentFilter))
}
exitLock = false
@@ -190,7 +189,7 @@ fun Activity.lock() {
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
cancelAll()
}
App.currentDatabase.closeAndClear(applicationContext)
App.currentDatabase.closeAndClear(applicationContext.filesDir)
setResult(LockingActivity.RESULT_EXIT_LOCK)
finish()
}

View File

@@ -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;
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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;
}
}

View File

@@ -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)
}
}

View File

@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.database.SortNodeEnum
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.Util
@@ -118,7 +119,7 @@ class NodeAdapter
}
private fun assignPreferences() {
val textSizeDefault = Util.getListTextDefaultSize(context)
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
this.textSize = PreferencesUtil.getListTextSize(context)
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
// Retrieve the icon size
@@ -207,7 +208,7 @@ class NodeAdapter
Type.GROUP -> iconGroupColor
Type.ENTRY -> iconEntryColor
}
mDatabase.drawFactory.assignDatabaseIconTo(context, holder.icon, subNode.icon, iconColor)
holder.icon?.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
// Assign text
holder.text?.text = subNode.title
// Assign click

View File

@@ -33,10 +33,12 @@ import com.kunzisoft.keepass.database.cursor.EntryCursor
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwIcon
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
class SearchEntryCursorAdapter(context: Context, private val database: Database) : CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
class SearchEntryCursorAdapter(context: Context, private val database: Database)
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
private val cursorInflater: LayoutInflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
@@ -89,7 +91,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
val viewHolder = view.tag as ViewHolder
// Assign image
database.drawFactory.assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor)
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
// Assign title
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
@@ -108,7 +110,7 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
return database.searchEntry(constraint.toString())
return database.searchEntries(constraint.toString())
}
fun getEntryFromPosition(position: Int): EntryVersioned? {

View File

@@ -37,7 +37,7 @@ class App : MultiDexApplication() {
}
override fun onTerminate() {
currentDatabase.closeAndClear(applicationContext)
currentDatabase.closeAndClear(applicationContext.filesDir)
super.onTerminate()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -17,20 +17,18 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto;
package com.kunzisoft.keepass.crypto
import java.security.Provider;
import java.security.Provider
public final class AESProvider extends Provider {
class AESProvider : Provider("AESProvider", 1.0, "") {
init {
put("Cipher.AES", NativeAESCipherSpi::class.java.name)
}
/**
*
*/
private static final long serialVersionUID = -3846349284296062658L;
companion object {
public AESProvider() {
super("AESProvider", 1.0, "");
put("Cipher.AES",NativeAESCipherSpi.class.getName());
private const val serialVersionUID = -3846349284296062658L
}
}

View File

@@ -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.");
}
}

View File

@@ -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.")
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -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();
}
}

View 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()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -47,14 +47,13 @@ public class NativeAESCipherSpi extends CipherSpi {
private static final String TAG = NativeAESCipherSpi.class.getName();
private static boolean mIsStaticInit = false;
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<PhantomReference<NativeAESCipherSpi>, Long>();
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<NativeAESCipherSpi>();
private static HashMap<PhantomReference<NativeAESCipherSpi>, Long> mCleanup = new HashMap<>();
private static ReferenceQueue<NativeAESCipherSpi> mQueue = new ReferenceQueue<>();
private final int AES_BLOCK_SIZE = 16;
private byte[] mIV;
private boolean mIsInited = false;
private boolean mEncrypting = false;
private boolean mIsInit = false;
private long mCtxPtr;
private boolean mPadding = false;
@@ -68,13 +67,12 @@ public class NativeAESCipherSpi extends CipherSpi {
private static void addToCleanupQueue(NativeAESCipherSpi ref, long ptr) {
Log.d(TAG, "queued cipher context: " + ptr);
mCleanup.put(new PhantomReference<NativeAESCipherSpi>(ref, mQueue), ptr);
mCleanup.put(new PhantomReference<>(ref, mQueue), ptr);
}
/** Work with the garbage collector to clean up openssl memory when the cipher
* context is garbage collected.
* @author bpellin
*
*/
private static class Cleanup implements Runnable {
@@ -92,13 +90,12 @@ public class NativeAESCipherSpi extends CipherSpi {
}
}
}
}
private static native void nCleanup(long ctxPtr);
public NativeAESCipherSpi() {
if ( ! mIsStaticInit ) {
if ( !mIsStaticInit ) {
staticInit();
}
}
@@ -134,11 +131,9 @@ public class NativeAESCipherSpi extends CipherSpi {
IllegalBlockSizeException, BadPaddingException {
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
if ( result == -1 ) {
throw new ShortBufferException();
}
return result;
}
@@ -146,7 +141,6 @@ public class NativeAESCipherSpi extends CipherSpi {
throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
int outputSize = engineGetOutputSize(inputLen);
int updateAmt;
if (input != null && inputLen > 0) {
updateAmt = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
@@ -155,11 +149,7 @@ public class NativeAESCipherSpi extends CipherSpi {
}
int finalAmt = nFinal(mCtxPtr, mPadding, output, outputOffset + updateAmt, outputSize - updateAmt);
int out = updateAmt + finalAmt;
return out;
return updateAmt + finalAmt;
}
private native int nFinal(long ctxPtr, boolean usePadding, byte[] output, int outputOffest, int outputSize)
@@ -231,22 +221,19 @@ public class NativeAESCipherSpi extends CipherSpi {
} catch (InvalidParameterSpecException e) {
throw new InvalidAlgorithmParameterException(e);
}
}
private void init(int opmode, Key key, IvParameterSpec params) {
if ( mIsInited ) {
if (mIsInit) {
// Do not allow multiple inits
assert(true);
throw new RuntimeException("Don't allow multiple inits");
} else {
NativeLib.init();
mIsInited = true;
NativeLib.INSTANCE.init();
mIsInit = true;
}
mIV = params.getIV();
mEncrypting = opmode == Cipher.ENCRYPT_MODE;
mCtxPtr = nInit(mEncrypting, key.getEncoded(), mIV);
mCtxPtr = nInit(opmode == Cipher.ENCRYPT_MODE, key.getEncoded(), mIV);
addToCleanupQueue(this, mCtxPtr);
}
@@ -263,26 +250,23 @@ public class NativeAESCipherSpi extends CipherSpi {
protected void engineSetPadding(String padding)
throws NoSuchPaddingException {
if ( ! mIsInited ) {
NativeLib.init();
if ( !mIsInit) {
NativeLib.INSTANCE.init();
}
if ( padding.length() == 0 ) {
return;
}
if ( ! padding.equals("PKCS5Padding") ) {
if ( !padding.equals("PKCS5Padding") ) {
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
}
mPadding = true;
}
@Override
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
int maxSize = engineGetOutputSize(inputLen);
byte output[] = new byte[maxSize];
byte[] output = new byte[maxSize];
int updateSize = update(input, inputOffset, inputLen, output, 0);
@@ -302,24 +286,15 @@ public class NativeAESCipherSpi extends CipherSpi {
byte[] output, int outputOffset) throws ShortBufferException {
int result = update(input, inputOffset, inputLen, output, outputOffset);
if ( result == -1 ) {
throw new ShortBufferException("Insufficient buffer.");
}
return result;
}
int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
private int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
int outputSize = engineGetOutputSize(inputLen);
int out = nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
return out;
return nUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
}
private native int nUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -17,30 +17,30 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.crypto;
package com.kunzisoft.keepass.crypto
public class NativeLib {
private static boolean isLoaded = false;
private static boolean loadSuccess = false;
object NativeLib {
private var isLoaded = false
private var loadSuccess = false
public static boolean loaded() {
return init();
fun loaded(): Boolean {
return init()
}
public static boolean init() {
if ( ! isLoaded ) {
fun init(): Boolean {
if (!isLoaded) {
try {
System.loadLibrary("final-key");
System.loadLibrary("argon2");
} catch ( UnsatisfiedLinkError e) {
return false;
}
isLoaded = true;
loadSuccess = true;
System.loadLibrary("final-key")
System.loadLibrary("argon2")
} catch (e: UnsatisfiedLinkError) {
return false
}
return loadSuccess;
isLoaded = true
loadSuccess = true
}
return loadSuccess
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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()))
}
}

View File

@@ -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;
}
}

View File

@@ -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()))
}
}

View File

@@ -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();
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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()))
}
}

View File

@@ -23,12 +23,8 @@ import com.kunzisoft.keepass.crypto.CipherFactory;
public class FinalKeyFactory {
public static FinalKey createFinalKey() {
return createFinalKey(false);
}
public static FinalKey createFinalKey(boolean androidOverride) {
// Prefer the native final key implementation
if ( !CipherFactory.deviceBlacklisted() && !androidOverride && NativeFinalKey.availble() ) {
if ( !CipherFactory.INSTANCE.deviceBlacklisted() && NativeFinalKey.availble() ) {
return new NativeFinalKey();
} else {
// Fall back on the android crypto implementation

View File

@@ -27,12 +27,12 @@ import java.io.IOException;
public class NativeFinalKey extends FinalKey {
public static boolean availble() {
return NativeLib.init();
return NativeLib.INSTANCE.init();
}
@Override
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
NativeLib.init();
NativeLib.INSTANCE.init();
return nTransformMasterKey(seed, key, rounds);

View File

@@ -71,11 +71,11 @@ public class AesKdf extends KdfEngine {
byte[] seed = p.getByteArray(ParamSeed);
if (masterKey.length != 32) {
masterKey = CryptoUtil.hashSha256(masterKey);
masterKey = CryptoUtil.INSTANCE.hashSha256(masterKey);
}
if (seed.length != 32) {
seed = CryptoUtil.hashSha256(seed);
seed = CryptoUtil.INSTANCE.hashSha256(seed);
}
FinalKey key = FinalKeyFactory.createFinalKey();

View File

@@ -28,7 +28,7 @@ public class Argon2Native {
public static byte[] transformKey(byte[] password, byte[] salt, int parallelism,
long memory, long iterations, byte[] secretKey,
byte[] associatedData, long version) throws IOException {
NativeLib.init();
NativeLib.INSTANCE.init();
return nTransformMasterKey(password, salt, parallelism, memory, iterations, secretKey, associatedData, version);
}

View File

@@ -24,10 +24,10 @@ import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getUriInputStream
import com.kunzisoft.keepass.utils.UriUtil
import java.io.IOException
class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
context: Context,
database: Database,
withMasterPassword: Boolean,
@@ -57,7 +57,7 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = getUriInputStream(context.contentResolver, mKeyFile)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
database.retrieveMasterKey(mMasterPassword, uriInputStream)
// To save the database
@@ -72,8 +72,8 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
}
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {
if (!isSuccess) {
override fun onFinishRun(result: Result) {
if (!result.isSuccess) {
// Erase the current master key
erase(database.masterKey)
mBackupKey?.let {
@@ -81,7 +81,7 @@ class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
}
}
super.onFinishRun(isSuccess, message)
super.onFinishRun(result)
}
/**

View File

@@ -19,30 +19,40 @@
*/
package com.kunzisoft.keepass.database.action
import com.kunzisoft.keepass.app.App
import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
class CreateDatabaseRunnable(private val mFilename: String,
val onDatabaseCreate: (database: Database) -> ActionRunnable)
: ActionRunnable() {
class CreateDatabaseRunnable(context: Context,
private val mDatabaseUri: Uri,
private val mDatabase: Database,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?,
save: Boolean,
actionRunnable: ActionRunnable? = null)
: AssignPasswordInDatabaseRunnable(context, mDatabase, withMasterPassword, masterPassword, withKeyFile, keyFile, save, actionRunnable) {
override fun run() {
try {
// Create new database record
Database(mFilename).apply {
App.currentDatabase = this
mDatabase.apply {
createData(mDatabaseUri)
// Set Database state
loaded = true
// Commit changes
onDatabaseCreate(this).run()
super.run()
}
finishRun(true)
} catch (e: Exception) {
mDatabase.closeAndClear()
finishRun(false, e.message)
}
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {}
override fun onFinishRun(result: Result) {}
}

View File

@@ -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"
}
}

View File

@@ -122,7 +122,11 @@ class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
FileDatabaseHistory.getInstance(mWeakContext).addDatabaseUri(uri, keyFileUri)
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {}
override fun onFinishRun(result: Result) {
if (!result.isSuccess) {
mDatabase.closeAndClear(mWeakContext.get()?.filesDir)
}
}
companion object {
private val TAG = LoadDatabaseRunnable::class.java.name

View File

@@ -1,11 +1,15 @@
package com.kunzisoft.keepass.database.action
import android.content.Intent
import android.os.AsyncTask
import android.os.Build
import android.support.annotation.StringRes
import android.support.v4.app.FragmentActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskNotificationService.Companion.DATABASE_TASK_TITLE_KEY
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper
open class ProgressDialogThread(private val activity: FragmentActivity,
private val actionRunnable: (ProgressTaskUpdater?)-> ActionRunnable,
@@ -18,18 +22,31 @@ open class ProgressDialogThread(private val activity: FragmentActivity,
messageId,
warningId)
private var actionRunnableAsyncTask: ActionRunnableAsyncTask? = null
var actionFinishInUIThread: ActionRunnable? = null
private var intentDatabaseTask:Intent = Intent(activity, DatabaseTaskNotificationService::class.java)
init {
actionRunnableAsyncTask = ActionRunnableAsyncTask(progressTaskDialogFragment,
{
activity.runOnUiThread {
intentDatabaseTask.putExtra(DATABASE_TASK_TITLE_KEY, titleId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intentDatabaseTask)
} else {
activity.startService(intentDatabaseTask)
}
TimeoutHelper.temporarilyDisableTimeout()
// Show the dialog
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
}
}, {
}, { result ->
activity.runOnUiThread {
actionFinishInUIThread?.onFinishRun(result)
// Remove the progress task
ProgressTaskDialogFragment.stop(activity)
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(activity)
activity.stopService(intentDatabaseTask)
}
})
}
@@ -41,24 +58,28 @@ open class ProgressDialogThread(private val activity: FragmentActivity,
private class ActionRunnableAsyncTask(private val progressTaskUpdater: ProgressTaskUpdater,
private val onPreExecute: () -> Unit,
private val onPostExecute: () -> Unit)
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, Void>() {
private val onPostExecute: (result: ActionRunnable.Result) -> Unit)
: AsyncTask<((ProgressTaskUpdater?)-> ActionRunnable), Void, ActionRunnable.Result>() {
override fun onPreExecute() {
super.onPreExecute()
onPreExecute.invoke()
}
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): Void? {
override fun doInBackground(vararg actionRunnables: ((ProgressTaskUpdater?)-> ActionRunnable)?): ActionRunnable.Result {
var resultTask = ActionRunnable.Result(false)
actionRunnables.forEach {
it?.invoke(progressTaskUpdater)?.run()
it?.invoke(progressTaskUpdater)?.apply {
run()
resultTask = result
}
return null
}
return resultTask
}
override fun onPostExecute(result: Void?) {
override fun onPostExecute(result: ActionRunnable.Result) {
super.onPostExecute(result)
onPostExecute.invoke()
onPostExecute.invoke(result)
}
}
}

View File

@@ -20,23 +20,16 @@
package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.PwDbOutputException
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.io.IOException
open class SaveDatabaseRunnable(protected var context: Context,
abstract class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
private val save: Boolean,
nestedAction: ActionRunnable? = null) : ActionRunnable(nestedAction) {
init {
TimeoutHelper.temporarilyDisableTimeout()
}
// TODO Service to prevent background thread kill
override fun run() {
if (save) {
@@ -52,8 +45,19 @@ open class SaveDatabaseRunnable(protected var context: Context,
// Need to call super.run() in child class
}
override fun onFinishRun(isSuccess: Boolean, message: String?) {
// Need to call super.onFinishRun(isSuccess, message) in child class
TimeoutHelper.releaseTemporarilyDisableTimeoutAndLockIfTimeout(context)
override fun onFinishRun(result: Result) {
// Need to call super.onFinishRun(result) in child class
}
}
class SaveDatabaseActionRunnable(context: Context,
database: Database,
save: Boolean,
nestedAction: ActionRunnable? = null)
: SaveDatabaseRunnable(context, database, save, nestedAction) {
override fun run() {
super.run()
finishRun(true)
}
}

View File

@@ -30,17 +30,17 @@ abstract class ActionNodeDatabaseRunnable(
/**
* Function do get the finish node action, don't implements onFinishRun() if used this
*/
abstract fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues
abstract fun nodeFinish(result: Result): ActionNodeValues
override fun onFinishRun(isSuccess: Boolean, message: String?) {
override fun onFinishRun(result: Result) {
callbackRunnable?.apply {
onActionNodeFinish(nodeFinish(isSuccess, message))
onActionNodeFinish(nodeFinish(result))
}
if (!isSuccess) {
if (!result.isSuccess) {
displayMessage(context)
}
super.onFinishRun(isSuccess, message)
super.onFinishRun(result)
}
}

View File

@@ -39,12 +39,12 @@ class AddEntryRunnable constructor(
database.addEntryTo(mNewEntry, mParent)
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
mNewEntry.parent?.let {
database.removeEntryFrom(mNewEntry, it)
}
}
return ActionNodeValues(isSuccess, message, null, mNewEntry)
return ActionNodeValues(result, null, mNewEntry)
}
}

View File

@@ -38,10 +38,10 @@ class AddGroupRunnable constructor(
database.addGroupTo(mNewGroup, mParent)
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
database.removeGroupFrom(mNewGroup, mParent)
}
return ActionNodeValues(isSuccess, message, null, mNewGroup)
return ActionNodeValues(result, null, mNewGroup)
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.action.node
import com.kunzisoft.keepass.database.element.NodeVersioned
import com.kunzisoft.keepass.tasks.ActionRunnable
/**
* Callback method who return the node(s) modified after an action
@@ -29,7 +30,7 @@ import com.kunzisoft.keepass.database.element.NodeVersioned
* - Move : @param oldNode NULL, @param NodeToMove
* - Update : @param oldNode NodeToUpdate, @param NodeUpdated
*/
data class ActionNodeValues(val success: Boolean, val message: String?, val oldNode: NodeVersioned?, val newNode: NodeVersioned?)
data class ActionNodeValues(val result: ActionRunnable.Result, val oldNode: NodeVersioned?, val newNode: NodeVersioned?)
abstract class AfterActionNodeFinishRunnable {
abstract fun onActionNodeFinish(actionNodeValues: ActionNodeValues)

View File

@@ -46,8 +46,8 @@ class CopyEntryRunnable constructor(
} ?: Log.e(TAG, "Unable to create a copy of the entry")
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, try to delete the copy
try {
mEntryCopied?.let {
@@ -58,7 +58,7 @@ class CopyEntryRunnable constructor(
}
}
return ActionNodeValues(isSuccess, message, mEntryToCopy, mEntryCopied)
return ActionNodeValues(result, mEntryToCopy, mEntryCopied)
}
companion object {

View File

@@ -33,7 +33,7 @@ class DeleteEntryRunnable constructor(
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
private var mParent: GroupVersioned? = null
private var mRecycle: Boolean = false
private var mCanRecycle: Boolean = false
override fun nodeAction() {
@@ -41,24 +41,24 @@ class DeleteEntryRunnable constructor(
mParent?.touch(modified = false, touchParents = true)
// Remove Entry from parent
mRecycle = database.canRecycle(mEntryToDelete)
if (mRecycle) {
database.recycle(mEntryToDelete)
mCanRecycle = database.canRecycle(mEntryToDelete)
if (mCanRecycle) {
database.recycle(mEntryToDelete, context.resources)
} else {
database.deleteEntry(mEntryToDelete)
}
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
mParent?.let {
if (mRecycle) {
if (mCanRecycle) {
database.undoRecycle(mEntryToDelete, it)
} else {
database.undoDeleteEntry(mEntryToDelete, it)
}
}
}
return ActionNodeValues(isSuccess, message, mEntryToDelete, null)
return ActionNodeValues(result, mEntryToDelete, null)
}
}

View File

@@ -39,14 +39,14 @@ class DeleteGroupRunnable(context: FragmentActivity,
// Remove Group from parent
mRecycle = database.canRecycle(mGroupToDelete)
if (mRecycle) {
database.recycle(mGroupToDelete)
database.recycle(mGroupToDelete, context.resources)
} else {
database.deleteGroup(mGroupToDelete)
}
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
if (mRecycle) {
mParent?.let {
database.undoRecycle(mGroupToDelete, it)
@@ -56,6 +56,6 @@ class DeleteGroupRunnable(context: FragmentActivity,
// TODO database.undoDeleteGroupFrom(mGroup, mParent);
}
}
return ActionNodeValues(isSuccess, message, mGroupToDelete, null)
return ActionNodeValues(result, mGroupToDelete, null)
}
}

View File

@@ -45,8 +45,8 @@ class MoveEntryRunnable constructor(
} ?: Log.e(TAG, "Unable to create a copy of the entry")
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, try to remove in the first place
try {
if (mEntryToMove != null && mOldParent != null)
@@ -56,7 +56,7 @@ class MoveEntryRunnable constructor(
}
}
return ActionNodeValues(isSuccess, message, null, mEntryToMove)
return ActionNodeValues(result, null, mEntryToMove)
}
companion object {

View File

@@ -53,8 +53,8 @@ class MoveGroupRunnable constructor(
} ?: Log.e(TAG, "Unable to create a copy of the group")
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, try to move in the first place
try {
if (mGroupToMove != null && mOldParent != null)
@@ -64,7 +64,7 @@ class MoveGroupRunnable constructor(
}
}
return ActionNodeValues(isSuccess, message, null, mGroupToMove)
return ActionNodeValues(result, null, mGroupToMove)
}
companion object {

View File

@@ -42,13 +42,13 @@ class UpdateEntryRunnable constructor(
mOldEntry.updateWith(mNewEntry)
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, back out changes to global structure
mBackupEntry?.let {
mOldEntry.updateWith(it)
}
}
return ActionNodeValues(isSuccess, message, mOldEntry, mNewEntry)
return ActionNodeValues(result, mOldEntry, mNewEntry)
}
}

View File

@@ -41,11 +41,11 @@ class UpdateGroupRunnable constructor(
mOldGroup.updateWith(mNewGroup)
}
override fun nodeFinish(isSuccess: Boolean, message: String?): ActionNodeValues {
if (!isSuccess) {
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, back out changes to global structure
mOldGroup.updateWith(mBackupGroup)
}
return ActionNodeValues(isSuccess, message, mOldGroup, mNewGroup)
return ActionNodeValues(result, mOldGroup, mNewGroup)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -43,9 +43,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.EmptyUtils
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.getUriInputStream
import org.apache.commons.io.FileUtils
import java.io.*
import java.util.*
@@ -60,8 +58,6 @@ class Database {
private var mUri: Uri? = null
private var searchHelper: SearchDbHelper? = null
var isReadOnly = false
var isPasswordEncodingError = false
private set
val drawFactory = IconDrawableFactory()
@@ -69,7 +65,7 @@ class Database {
val iconFactory: PwIconFactory
get() {
return pwDatabaseV3?.getIconFactory() ?: pwDatabaseV4?.getIconFactory() ?: PwIconFactory()
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
}
val name: String
@@ -110,13 +106,13 @@ class Database {
return ArrayList()
}
val kdfEngine: KdfEngine?
val kdfEngine: KdfEngine
get() {
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
}
val numberKeyEncryptionRoundsAsString: String
get() = java.lang.Long.toString(numberKeyEncryptionRounds)
get() = numberKeyEncryptionRounds.toString()
var numberKeyEncryptionRounds: Long
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
@@ -127,7 +123,7 @@ class Database {
}
val memoryUsageAsString: String
get() = java.lang.Long.toString(memoryUsage)
get() = memoryUsage.toString()
var memoryUsage: Long
get() {
@@ -138,7 +134,7 @@ class Database {
}
val parallelismAsString: String
get() = Integer.toString(parallelism)
get() = parallelism.toString()
var parallelism: Int
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOW_VALUE
@@ -147,7 +143,7 @@ class Database {
}
var masterKey: ByteArray
get() = pwDatabaseV3?.getMasterKey() ?: pwDatabaseV4?.getMasterKey() ?: ByteArray(32)
get() = pwDatabaseV3?.masterKey ?: pwDatabaseV4?.masterKey ?: ByteArray(32)
set(masterKey) {
pwDatabaseV3?.masterKey = masterKey
pwDatabaseV4?.masterKey = masterKey
@@ -155,11 +151,11 @@ class Database {
val rootGroup: GroupVersioned?
get() {
pwDatabaseV3?.let {
return GroupVersioned(it.rootGroup)
pwDatabaseV3?.rootGroup?.let {
return GroupVersioned(it)
}
pwDatabaseV4?.let {
return GroupVersioned(it.rootGroup)
pwDatabaseV4?.rootGroup?.let {
return GroupVersioned(it)
}
return null
}
@@ -182,24 +178,6 @@ class Database {
return null
}
constructor()
constructor(databasePath: String) {
// TODO Test with kdb extension
if (isKDBExtension(databasePath)) {
setDatabaseV3(PwDatabaseV3())
} else {
val databaseV4 = PwDatabaseV4()
val groupV4 = databaseV4.createGroup()
groupV4.title = dbNameFromPath(databasePath)
groupV4.icon = databaseV4.getIconFactory().folderIcon
databaseV4.rootGroup = groupV4
setDatabaseV4(databaseV4)
}
setUri(UriUtil.parseDefaultFile(databasePath))
}
private fun setDatabaseV3(pwDatabaseV3: PwDatabaseV3) {
this.pwDatabaseV3 = pwDatabaseV3
this.pwDatabaseV4 = null
@@ -210,17 +188,9 @@ class Database {
this.pwDatabaseV4 = pwDatabaseV4
}
private fun isKDBExtension(filename: String?): Boolean {
if (filename == null) {
return false
}
val extIdx = filename.lastIndexOf(".")
return if (extIdx == -1) false else filename.substring(extIdx).equals(".kdb", ignoreCase = true)
}
private fun dbNameFromPath(dbPath: String): String {
val filename = URLUtil.guessFileName(dbPath, null, null)
if (EmptyUtils.isNullOrEmpty(filename)) {
private fun dbNameFromUri(databaseUri: Uri): String {
val filename = URLUtil.guessFileName(databaseUri.path, null, null)
if (filename == null || filename.isEmpty()) {
return "KeePass Database"
}
val lastExtDot = filename.lastIndexOf(".")
@@ -229,8 +199,10 @@ class Database {
} else filename.substring(0, lastExtDot)
}
fun setUri(mUri: Uri) {
this.mUri = mUri
fun createData(databaseUri: Uri) {
// Always create a new database with the last version
setDatabaseV4(PwDatabaseV4(dbNameFromUri(databaseUri)))
this.mUri = databaseUri
}
@Throws(IOException::class, InvalidDBException::class)
@@ -246,7 +218,7 @@ class Database {
// Pass Uris as InputStreams
val inputStream: InputStream?
try {
inputStream = getUriInputStream(ctx.contentResolver, uri)
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw ContentFileNotFoundException.getInstance(uri)
@@ -256,7 +228,7 @@ class Database {
var keyFileInputStream: InputStream? = null
keyfile?.let {
try {
keyFileInputStream = getUriInputStream(ctx.contentResolver, keyfile)
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw ContentFileNotFoundException.getInstance(keyfile)
@@ -300,7 +272,6 @@ class Database {
}
try {
isPasswordEncodingError = !(pwDatabaseV3?.validatePasswordEncoding(password) ?: pwDatabaseV4?.validatePasswordEncoding(password) ?: true)
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
loaded = true
} catch (e: Exception) {
@@ -320,7 +291,7 @@ class Database {
return searchHelper?.search(this, str, max)
}
fun searchEntry(query: String): Cursor? {
fun searchEntries(query: String): Cursor? {
var cursorV3: EntryCursorV3? = null
var cursorV4: EntryCursorV4? = null
@@ -348,7 +319,7 @@ class Database {
}
fun getEntryFrom(cursor: Cursor): EntryVersioned? {
val iconFactory = pwDatabaseV3?.getIconFactory() ?: pwDatabaseV4?.getIconFactory() ?: PwIconFactory()
val iconFactory = pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
val entry = createEntry()
// TODO invert field reference manager
@@ -428,21 +399,22 @@ class Database {
}
// TODO Clear database when lock broadcast is receive in backstage
fun closeAndClear(context: Context) {
fun closeAndClear(filesDirectory: File? = null) {
drawFactory.clearCache()
// Delete the cache of the database if present
pwDatabaseV3?.clearCache()
pwDatabaseV4?.clearCache()
// In all cases, delete all the files in the temp dir
try {
FileUtils.cleanDirectory(context.filesDir)
FileUtils.cleanDirectory(filesDirectory)
} catch (e: IOException) {
Log.e(TAG, "Unable to clear the directory cache.", e)
}
pwDatabaseV3 = null
pwDatabaseV4 = null
mUri = null
loaded = false
isPasswordEncodingError = false
}
fun getVersion(): String {
@@ -496,14 +468,13 @@ class Database {
}
fun getKeyDerivationName(resources: Resources): String {
val kdfEngine = kdfEngine
return if (kdfEngine != null) {
kdfEngine.getName(resources)
} else ""
return kdfEngine.getName(resources)
}
fun validatePasswordEncoding(key: String): Boolean {
return pwDatabaseV3?.validatePasswordEncoding(key) ?: pwDatabaseV4?.validatePasswordEncoding(key) ?: false
fun validatePasswordEncoding(key: String?): Boolean {
return pwDatabaseV3?.validatePasswordEncoding(key)
?: pwDatabaseV4?.validatePasswordEncoding(key)
?: false
}
@Throws(InvalidKeyFileException::class, IOException::class)
@@ -515,16 +486,12 @@ class Database {
fun createEntry(): EntryVersioned? {
pwDatabaseV3?.let { database ->
return EntryVersioned(database.createEntry()).apply {
database.newEntryId()?.let {
nodeId = it
}
nodeId = database.newEntryId()
}
}
pwDatabaseV4?.let { database ->
return EntryVersioned(database.createEntry()).apply {
database.newEntryId()?.let {
nodeId = it
}
nodeId = database.newEntryId()
}
}
@@ -534,16 +501,12 @@ class Database {
fun createGroup(): GroupVersioned? {
pwDatabaseV3?.let { database ->
return GroupVersioned(database.createGroup()).apply {
database.newGroupId()?.let {
setNodeId(it)
}
setNodeId(database.newGroupId())
}
}
pwDatabaseV4?.let { database ->
return GroupVersioned(database.createGroup()).apply {
database.newGroupId()?.let {
setNodeId(it)
}
setNodeId(database.newGroupId())
}
}
@@ -571,26 +534,42 @@ class Database {
}
fun addEntryTo(entry: EntryVersioned, parent: GroupVersioned) {
pwDatabaseV3?.addEntryTo(entry.pwEntryV3, parent.pwGroupV3)
pwDatabaseV4?.addEntryTo(entry.pwEntryV4, parent.pwGroupV4)
entry.pwEntryV3?.let { entryV3 ->
pwDatabaseV3?.addEntryTo(entryV3, parent.pwGroupV3)
}
entry.pwEntryV4?.let { entryV4 ->
pwDatabaseV4?.addEntryTo(entryV4, parent.pwGroupV4)
}
entry.afterAssignNewParent()
}
fun removeEntryFrom(entry: EntryVersioned, parent: GroupVersioned) {
pwDatabaseV3?.removeEntryFrom(entry.pwEntryV3, parent.pwGroupV3)
pwDatabaseV4?.removeEntryFrom(entry.pwEntryV4, parent.pwGroupV4)
entry.pwEntryV3?.let { entryV3 ->
pwDatabaseV3?.removeEntryFrom(entryV3, parent.pwGroupV3)
}
entry.pwEntryV4?.let { entryV4 ->
pwDatabaseV4?.removeEntryFrom(entryV4, parent.pwGroupV4)
}
entry.afterAssignNewParent()
}
fun addGroupTo(group: GroupVersioned, parent: GroupVersioned) {
pwDatabaseV3?.addGroupTo(group.pwGroupV3, parent.pwGroupV3)
pwDatabaseV4?.addGroupTo(group.pwGroupV4, parent.pwGroupV4)
group.pwGroupV3?.let { groupV3 ->
pwDatabaseV3?.addGroupTo(groupV3, parent.pwGroupV3)
}
group.pwGroupV4?.let { groupV4 ->
pwDatabaseV4?.addGroupTo(groupV4, parent.pwGroupV4)
}
group.afterAssignNewParent()
}
fun removeGroupFrom(group: GroupVersioned, parent: GroupVersioned) {
pwDatabaseV3?.removeGroupFrom(group.pwGroupV3, parent.pwGroupV3)
pwDatabaseV4?.removeGroupFrom(group.pwGroupV4, parent.pwGroupV4)
group.pwGroupV3?.let { groupV3 ->
pwDatabaseV3?.removeGroupFrom(groupV3, parent.pwGroupV3)
}
group.pwGroupV4?.let { groupV4 ->
pwDatabaseV4?.removeGroupFrom(groupV4, parent.pwGroupV4)
}
group.afterAssignNewParent()
}
@@ -647,37 +626,65 @@ class Database {
}
fun undoDeleteEntry(entry: EntryVersioned, parent: GroupVersioned) {
pwDatabaseV3?.undoDeleteEntryFrom(entry.pwEntryV3, parent.pwGroupV3)
pwDatabaseV4?.undoDeleteEntryFrom(entry.pwEntryV4, parent.pwGroupV4)
entry.pwEntryV3?.let { entryV3 ->
pwDatabaseV3?.undoDeleteEntryFrom(entryV3, parent.pwGroupV3)
}
entry.pwEntryV4?.let { entryV4 ->
pwDatabaseV4?.undoDeleteEntryFrom(entryV4, parent.pwGroupV4)
}
}
fun undoDeleteGroup(group: GroupVersioned, parent: GroupVersioned) {
pwDatabaseV3?.undoDeleteGroupFrom(group.pwGroupV3, parent.pwGroupV3)
pwDatabaseV4?.undoDeleteGroupFrom(group.pwGroupV4, parent.pwGroupV4)
group.pwGroupV3?.let { groupV3 ->
pwDatabaseV3?.undoDeleteGroupFrom(groupV3, parent.pwGroupV3)
}
group.pwGroupV4?.let { groupV4 ->
pwDatabaseV4?.undoDeleteGroupFrom(groupV4, parent.pwGroupV4)
}
}
fun canRecycle(entry: EntryVersioned): Boolean {
return pwDatabaseV4?.canRecycle(entry.pwEntryV4) ?: false
var canRecycle: Boolean? = null
entry.pwEntryV4?.let { entryV4 ->
canRecycle = pwDatabaseV4?.canRecycle(entryV4)
}
return canRecycle ?: false
}
fun canRecycle(group: GroupVersioned): Boolean {
return pwDatabaseV4?.canRecycle(group.pwGroupV4) ?: false
var canRecycle: Boolean? = null
group.pwGroupV4?.let { groupV4 ->
canRecycle = pwDatabaseV4?.canRecycle(groupV4)
}
return canRecycle ?: false
}
fun recycle(entry: EntryVersioned) {
pwDatabaseV4?.recycle(entry.pwEntryV4)
fun recycle(entry: EntryVersioned, resources: Resources) {
entry.pwEntryV4?.let {
pwDatabaseV4?.recycle(it, resources)
}
}
fun recycle(group: GroupVersioned) {
pwDatabaseV4?.recycle(group.pwGroupV4)
fun recycle(group: GroupVersioned, resources: Resources) {
group.pwGroupV4?.let {
pwDatabaseV4?.recycle(it, resources)
}
}
fun undoRecycle(entry: EntryVersioned, parent: GroupVersioned) {
pwDatabaseV4?.undoRecycle(entry.pwEntryV4, parent.pwGroupV4)
entry.pwEntryV4?.let { entryV4 ->
parent.pwGroupV4?.let { parentV4 ->
pwDatabaseV4?.undoRecycle(entryV4, parentV4)
}
}
}
fun undoRecycle(group: GroupVersioned, parent: GroupVersioned) {
pwDatabaseV4?.undoRecycle(group.pwGroupV4, parent.pwGroupV4)
group.pwGroupV4?.let { groupV4 ->
parent.pwGroupV4?.let { parentV4 ->
pwDatabaseV4?.undoRecycle(groupV4, parentV4)
}
}
}
fun startManageEntry(entry: EntryVersioned) {

View File

@@ -304,4 +304,22 @@ class GroupVersioned : NodeVersioned, PwGroupInterface<GroupVersioned, EntryVers
fun containsCustomData(): Boolean {
return pwGroupV4?.containsCustomData() ?: false
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GroupVersioned
if (pwGroupV3 != other.pwGroupV3) return false
if (pwGroupV4 != other.pwGroupV4) return false
return true
}
override fun hashCode(): Int {
var result = pwGroupV3?.hashCode() ?: 0
result = 31 * result + (pwGroupV4?.hashCode() ?: 0)
return result
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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));
}
}

View File

@@ -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)
}
}
}

View File

@@ -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();
}
}

View File

@@ -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"
}
}

View File

@@ -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;
}
};
}

View File

@@ -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
}
}
}

View File

@@ -24,7 +24,6 @@ import android.os.Parcelable
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.utils.MemUtil
import com.kunzisoft.keepass.utils.SprEngineV4
import java.util.*
class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
@@ -167,7 +166,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
}
/**
* Decode a reference key woth the SprEngineV4
* Decode a reference key with the SprEngineV4
* @param decodeRef
* @param key
* @return
@@ -175,7 +174,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, NodeV4Interface {
private fun decodeRefKey(decodeRef: Boolean, key: String): String {
val text = fields.getProtectedStringValue(key)
return if (decodeRef) {
if (mDatabase == null) text else SprEngineV4().compile(text, this, mDatabase)
if (mDatabase == null) text else SprEngineV4().compile(text, this, mDatabase!!)
} else text
}

View File

@@ -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 = "}"
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*
@@ -20,14 +20,8 @@
package com.kunzisoft.keepass.database.exception
import android.net.Uri
import com.kunzisoft.keepass.utils.EmptyUtils
import java.io.FileNotFoundException
/**
* Created by bpellin on 3/14/16.
*/
class ContentFileNotFoundException : FileNotFoundException() {
companion object {
fun getInstance(uri: Uri?): FileNotFoundException {
@@ -36,11 +30,11 @@ class ContentFileNotFoundException : FileNotFoundException() {
}
val scheme = uri.scheme
return if (!EmptyUtils.isNullOrEmpty(scheme) && scheme!!.equals("content", ignoreCase = true)) {
return if (scheme != null
&& scheme.isNotEmpty()
&& scheme.equals("content", ignoreCase = true)) {
ContentFileNotFoundException()
} else FileNotFoundException()
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePass DX.
*

Some files were not shown because too many files have changed in this diff Show More