Merge branch 'develop' into Bug_Feature_Template

This commit is contained in:
J-Jamet
2019-10-04 16:22:37 +02:00
150 changed files with 2828 additions and 1836 deletions

View File

@@ -1,3 +1,11 @@
KeepassDX (2.5.0.0beta24)
* Add settings (Color, Security, Master Key)
* Show history of each entry
* Auto repair database for nodes with same UUID
* Fix settings
* Fix edit group
* Fix small bugs
KeepassDX (2.5.0.0beta23) KeepassDX (2.5.0.0beta23)
* New, more secure database creation workflow * New, more secure database creation workflow
* Recognize more database files * Recognize more database files

View File

@@ -6,13 +6,14 @@ apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 28 compileSdkVersion 28
buildToolsVersion '28.0.3' buildToolsVersion '28.0.3'
ndkVersion "20.0.5594570"
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 28 targetSdkVersion 28
versionCode = 23 versionCode = 24
versionName = "2.5.0.0beta23" versionName = "2.5.0.0beta24"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -89,7 +90,7 @@ dependencies {
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.biometric:biometric:1.0.0-beta01' implementation 'androidx.biometric:biometric:1.0.0-beta02'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
@@ -97,10 +98,10 @@ dependencies {
implementation "com.madgag.spongycastle:core:$spongycastleVersion" implementation "com.madgag.spongycastle:core:$spongycastleVersion"
implementation "com.madgag.spongycastle:prov:$spongycastleVersion" implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
// Expandable view
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
// Time // Time
implementation 'joda-time:joda-time:2.9.9' implementation 'joda-time:joda-time:2.9.9'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
// Education // Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0' implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
// Apache Commons Collections // Apache Commons Collections

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
@@ -49,15 +50,19 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.EntryContentsView import com.kunzisoft.keepass.view.EntryContentsView
import java.util.*
class EntryActivity : LockingHideActivity() { class EntryActivity : LockingHideActivity() {
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null private var titleIconView: ImageView? = null
private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null private var entryContentsView: EntryContentsView? = null
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var mEntry: EntryVersioned? = null private var mEntry: EntryVersioned? = null
private var mIsHistory: Boolean = false
private var mShowPassword: Boolean = false private var mShowPassword: Boolean = false
private var clipboardHelper: ClipboardHelper? = null private var clipboardHelper: ClipboardHelper? = null
@@ -82,12 +87,18 @@ class EntryActivity : LockingHideActivity() {
// Get Entry from UUID // Get Entry from UUID
try { try {
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY) val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = currentDatabase.getEntryById(keyEntry) mEntry = currentDatabase.getEntryById(keyEntry)
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
Log.e(TAG, "Unable to retrieve the entry key") Log.e(TAG, "Unable to retrieve the entry key")
} }
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
if (historyPosition >= 0) {
mIsHistory = true
mEntry = mEntry?.getHistory()?.get(historyPosition)
}
if (mEntry == null) { if (mEntry == null) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish() finish()
@@ -108,6 +119,7 @@ class EntryActivity : LockingHideActivity() {
// Get views // Get views
collapsingToolbarLayout = findViewById(R.id.toolbar_layout) collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon) titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents) entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
@@ -238,18 +250,11 @@ class EntryActivity : LockingHideActivity() {
entryContentsView?.setHiddenPasswordStyle(!mShowPassword) entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
// Assign dates // Assign dates
entry.creationTime.date?.let { entryContentsView?.assignCreationDate(entry.creationTime)
entryContentsView?.assignCreationDate(it) entryContentsView?.assignModificationDate(entry.lastModificationTime)
} entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
entry.lastModificationTime.date?.let { if (entry.isExpires) {
entryContentsView?.assignModificationDate(it) entryContentsView?.assignExpiresDate(entry.expiryTime)
}
entry.lastAccessTime.date?.let {
entryContentsView?.assignLastAccessDate(it)
}
val expires = entry.expiryTime.date
if (entry.isExpires && expires != null) {
entryContentsView?.assignExpiresDate(expires)
} else { } else {
entryContentsView?.assignExpiresDate(getString(R.string.never)) entryContentsView?.assignExpiresDate(getString(R.string.never))
} }
@@ -257,6 +262,24 @@ class EntryActivity : LockingHideActivity() {
// Assign special data // Assign special data
entryContentsView?.assignUUID(entry.nodeId.id) entryContentsView?.assignUUID(entry.nodeId.id)
// Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
if (mIsHistory) {
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
}
val entryHistory = entry.getHistory()
// isMainEntry = not an history
val showHistoryView = entryHistory.isNotEmpty()
entryContentsView?.showHistory(showHistoryView)
if (showHistoryView) {
entryContentsView?.assignHistory(entryHistory)
entryContentsView?.onHistoryClick { historyItem, position ->
launch(this, historyItem, true, position)
}
}
database.stopManageEntry(entry) database.stopManageEntry(entry)
} }
@@ -412,13 +435,16 @@ class EntryActivity : LockingHideActivity() {
companion object { companion object {
private val TAG = EntryActivity::class.java.name private val TAG = EntryActivity::class.java.name
const val KEY_ENTRY = "entry" const val KEY_ENTRY = "KEY_ENTRY"
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) { fun launch(activity: Activity, entry: EntryVersioned, readOnly: Boolean, historyPosition: Int? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java) val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, pw.nodeId) intent.putExtra(KEY_ENTRY, entry.nodeId)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
if (historyPosition != null)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE) activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
} }
} }

View File

@@ -44,6 +44,7 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView import com.kunzisoft.keepass.view.EntryEditContentsView
import java.util.*
class EntryEditActivity : LockingHideActivity(), class EntryEditActivity : LockingHideActivity(),
IconPickerDialogFragment.IconPickerListener, IconPickerDialogFragment.IconPickerListener,
@@ -90,7 +91,7 @@ class EntryEditActivity : LockingHideActivity(),
mDatabase = Database.getInstance() mDatabase = Database.getInstance()
// Entry is retrieve, it's an entry to update // Entry is retrieve, it's an entry to update
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let { intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false mIsNew = false
// Create an Entry copy to modify from the database entry // Create an Entry copy to modify from the database entry
mEntry = mDatabase?.getEntryById(it) mEntry = mDatabase?.getEntryById(it)
@@ -105,14 +106,12 @@ class EntryEditActivity : LockingHideActivity(),
} }
} }
// Retrieve the icon after an orientation change // Create the new entry from the current one
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) { if (savedInstanceState == null
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned || !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
} else {
mEntry?.let { entry -> mEntry?.let { entry ->
// Create a copy to modify // Create a copy to modify
mNewEntry = EntryVersioned(entry).also { newEntry -> mNewEntry = EntryVersioned(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable // WARNING Remove the parent to keep memory with parcelable
newEntry.parent = null newEntry.parent = null
} }
@@ -123,7 +122,11 @@ class EntryEditActivity : LockingHideActivity(),
// Parent is retrieve, it's a new entry to create // Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let { intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
mIsNew = true mIsNew = true
mNewEntry = mDatabase?.createEntry() // Create an empty new entry
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = mDatabase?.createEntry()
}
mParent = mDatabase?.getGroupById(it) mParent = mDatabase?.getGroupById(it)
// Add the default icon // Add the default icon
mDatabase?.drawFactory?.let { iconFactory -> mDatabase?.drawFactory?.let { iconFactory ->
@@ -131,6 +134,12 @@ class EntryEditActivity : LockingHideActivity(),
} }
} }
// Retrieve the new entry after an orientation change
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
}
// Close the activity if entry or parent can't be retrieve // Close the activity if entry or parent can't be retrieve
if (mNewEntry == null || mParent == null) { if (mNewEntry == null || mParent == null) {
finish() finish()
@@ -168,12 +177,14 @@ class EntryEditActivity : LockingHideActivity(),
// Set info in view // Set info in view
entryEditContentsView?.apply { entryEditContentsView?.apply {
title = newEntry.title title = newEntry.title
username = newEntry.username username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url url = newEntry.url
password = newEntry.password password = newEntry.password
notes = newEntry.notes notes = newEntry.notes
for (entry in newEntry.customFields.entries) { for (entry in newEntry.customFields.entries) {
addNewCustomField(entry.key, entry.value) post {
addNewCustomField(entry.key, entry.value)
}
} }
} }
} }
@@ -185,6 +196,7 @@ class EntryEditActivity : LockingHideActivity(),
newEntry.apply { newEntry.apply {
// Build info from view // Build info from view
entryEditContentsView?.let { entryView -> entryEditContentsView?.let { entryView ->
removeAllFields()
title = entryView.title title = entryView.title
username = entryView.username username = entryView.username
url = entryView.url url = entryView.url
@@ -218,8 +230,6 @@ class EntryEditActivity : LockingHideActivity(),
*/ */
private fun addNewCustomField() { private fun addNewCustomField() {
entryEditContentsView?.addNewCustomField() entryEditContentsView?.addNewCustomField()
// Scroll bottom
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
} }
/** /**
@@ -342,7 +352,10 @@ class EntryEditActivity : LockingHideActivity(),
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry) mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }

View File

@@ -39,7 +39,6 @@ import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
@@ -54,13 +53,11 @@ import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
import com.kunzisoft.keepass.database.action.ProgressDialogThread import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.* import kotlinx.android.synthetic.main.activity_file_selection.*
import net.cachapa.expandablelayout.ExpandableLayout
import java.io.FileNotFoundException import java.io.FileNotFoundException
class FileDatabaseSelectActivity : StylishActivity(), class FileDatabaseSelectActivity : StylishActivity(),
@@ -69,11 +66,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
// Views // Views
private var fileListContainer: View? = null private var fileListContainer: View? = null
private var createButtonView: View? = null private var createButtonView: View? = null
private var browseButtonView: View? = null private var openDatabaseButtonView: View? = null
private var openButtonView: View? = null
private var fileSelectExpandableButtonView: View? = null
private var fileSelectExpandableLayout: ExpandableLayout? = null
private var openFileNameView: EditText? = null
// Adapter to manage database history list // Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -84,8 +77,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
private var mOpenFileHelper: OpenFileHelper? = null private var mOpenFileHelper: OpenFileHelper? = null
private var mDefaultPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -98,44 +89,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
toolbar.title = "" toolbar.title = ""
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
openFileNameView = findViewById(R.id.file_filename)
// Set the initial value of the filename
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
+ getString(R.string.database_file_path_default)
+ getString(R.string.database_file_name_default)
+ getString(R.string.database_file_extension_default))
openFileNameView?.setHint(R.string.open_link_database)
// Button to expand file selection
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
fileSelectExpandableButtonView?.setOnClickListener { _ ->
if (fileSelectExpandableLayout?.isExpanded == true)
fileSelectExpandableLayout?.collapse()
else
fileSelectExpandableLayout?.expand()
}
// Open button
openButtonView = findViewById(R.id.open_database)
openButtonView?.setOnClickListener { _ ->
var fileName = openFileNameView?.text?.toString() ?: ""
mDefaultPath?.let {
if (fileName.isEmpty())
fileName = it
}
UriUtil.parse(fileName)?.let { fileNameUri ->
launchPasswordActivityWithPath(fileNameUri)
} ?: run {
Log.e(TAG, "Unable to open the database link")
Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show()
null
}
}
// Create button // Create button
createButtonView = findViewById(R.id.create_database) createButtonView = findViewById(R.id.create_database_button)
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply { if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass" type = "application/x-keepass"
@@ -151,10 +106,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
createButtonView?.setOnClickListener { createNewFile() } createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this) mOpenFileHelper = OpenFileHelper(this)
browseButtonView = findViewById(R.id.browse_button) openDatabaseButtonView = findViewById(R.id.open_database_button)
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener { openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
UriUtil.parse(openFileNameView?.text?.toString())
})
// History list // History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list) val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -358,7 +311,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
private val keyFileUri: Uri?) : ActionRunnable() { private val keyFileUri: Uri?) : ActionRunnable() {
override fun run() { override fun run() {
finishRun(true, null) finishRun(true)
} }
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
@@ -392,12 +345,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri -> ) { uri ->
if (uri != null) { if (uri != null) {
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) { launchPasswordActivityWithPath(uri)
launchPasswordActivityWithPath(uri)
} else {
fileSelectExpandableLayout?.expand(false)
openFileNameView?.setText(uri.toString())
}
} }
} }
@@ -405,7 +353,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mDatabaseFileUri = data?.data mDatabaseFileUri = data?.data
if (mDatabaseFileUri != null) { if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog") AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} }
// else { // else {
// TODO Show error // TODO Show error
@@ -438,20 +387,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
}) })
if (!createDatabaseEducationPerformed) { if (!createDatabaseEducationPerformed) {
// selectDatabaseEducationPerformed // selectDatabaseEducationPerformed
browseButtonView != null openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation( && fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!, openDatabaseButtonView!!,
{tapTargetView -> {tapTargetView ->
tapTargetView?.let { tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it) mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
} }
}, },
{ {}
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
) )
} }
} }

View File

@@ -18,7 +18,6 @@
*/ */
package com.kunzisoft.keepass.activities package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.SearchManager import android.app.SearchManager
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
@@ -29,16 +28,16 @@ import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentManager
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
@@ -62,7 +61,8 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.view.AddNodeButtonView import com.kunzisoft.keepass.view.AddNodeButtonView
import net.cachapa.expandablelayout.ExpandableLayout import com.kunzisoft.keepass.view.collapse
import com.kunzisoft.keepass.view.expand
class GroupActivity : LockingActivity(), class GroupActivity : LockingActivity(),
GroupEditDialogFragment.EditGroupListener, GroupEditDialogFragment.EditGroupListener,
@@ -75,7 +75,6 @@ class GroupActivity : LockingActivity(),
// Views // Views
private var toolbar: Toolbar? = null private var toolbar: Toolbar? = null
private var searchTitleView: View? = null private var searchTitleView: View? = null
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
private var toolbarPaste: Toolbar? = null private var toolbarPaste: Toolbar? = null
private var iconView: ImageView? = null private var iconView: ImageView? = null
private var modeTitleView: TextView? = null private var modeTitleView: TextView? = null
@@ -115,10 +114,21 @@ class GroupActivity : LockingActivity(),
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
searchTitleView = findViewById(R.id.search_title) searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name) groupNameView = findViewById(R.id.group_name)
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
toolbarPaste = findViewById(R.id.toolbar_paste) toolbarPaste = findViewById(R.id.toolbar_paste)
modeTitleView = findViewById(R.id.mode_title_view) modeTitleView = findViewById(R.id.mode_title_view)
toolbar?.title = ""
setSupportActionBar(toolbar)
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
toolbarPaste?.collapse(false)
toolbarPaste?.setNavigationOnClickListener {
toolbarPaste?.collapse()
mNodeToCopy = null
mNodeToMove = null
}
// Focus view to reinitialize timeout // Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView) resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
@@ -129,9 +139,11 @@ class GroupActivity : LockingActivity(),
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) { if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY) mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener()) toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
toolbarPaste?.expand(false)
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) { } else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY) mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener()) toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
toolbarPaste?.expand(false)
} }
} }
@@ -153,17 +165,6 @@ class GroupActivity : LockingActivity(),
// Update last access time. // Update last access time.
mCurrentGroup?.touch(modified = false, touchParents = false) mCurrentGroup?.touch(modified = false, touchParents = false)
toolbar?.title = ""
setSupportActionBar(toolbar)
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
toolbarPaste?.setNavigationOnClickListener {
toolbarPasteExpandableLayout?.collapse()
mNodeToCopy = null
mNodeToMove = null
}
// Retrieve the textColor to tint the icon // Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE) mIconColor = taTextColor.getColor(0, Color.WHITE)
@@ -449,7 +450,7 @@ class GroupActivity : LockingActivity(),
} }
override fun onCopyMenuClick(node: NodeVersioned): Boolean { override fun onCopyMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand() toolbarPaste?.expand()
mNodeToCopy = node mNodeToCopy = node
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener()) toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
return false return false
@@ -457,7 +458,7 @@ class GroupActivity : LockingActivity(),
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener { private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse() toolbarPaste?.collapse()
when (item.itemId) { when (item.itemId) {
R.id.menu_paste -> { R.id.menu_paste -> {
@@ -489,7 +490,7 @@ class GroupActivity : LockingActivity(),
} }
override fun onMoveMenuClick(node: NodeVersioned): Boolean { override fun onMoveMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand() toolbarPaste?.expand()
mNodeToMove = node mNodeToMove = node
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener()) toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
return false return false
@@ -497,7 +498,7 @@ class GroupActivity : LockingActivity(),
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener { private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse() toolbarPaste?.collapse()
when (item.itemId) { when (item.itemId) {
R.id.menu_paste -> { R.id.menu_paste -> {

View File

@@ -44,6 +44,7 @@ import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.* import android.widget.*
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -61,6 +62,8 @@ import com.kunzisoft.keepass.database.action.ProgressDialogThread
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
import com.kunzisoft.keepass.database.search.SearchDbHelper
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
@@ -69,7 +72,6 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_password.* import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.lang.ref.WeakReference
class PasswordActivity : StylishActivity() { class PasswordActivity : StylishActivity() {
@@ -87,6 +89,8 @@ class PasswordActivity : StylishActivity() {
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var prefs: SharedPreferences? = null private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
@@ -101,8 +105,7 @@ class PasswordActivity : StylishActivity() {
prefs = PreferenceManager.getDefaultSharedPreferences(this) prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key), mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
resources.getBoolean(R.bool.keyfile_default))
setContentView(R.layout.activity_password) setContentView(R.layout.activity_password)
@@ -123,7 +126,7 @@ class PasswordActivity : StylishActivity() {
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
val browseView = findViewById<View>(R.id.browse_button) val browseView = findViewById<View>(R.id.open_database_button)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity) mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener) browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
@@ -222,6 +225,7 @@ class PasswordActivity : StylishActivity() {
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title // Define title
databaseFileUri?.let { databaseFileUri?.let {
@@ -243,11 +247,13 @@ class PasswordActivity : StylishActivity() {
newDefaultFileName = databaseFileUri ?: newDefaultFileName newDefaultFileName = databaseFileUri ?: newDefaultFileName
} }
newDefaultFileName?.let { prefs?.edit()?.apply {
prefs?.edit()?.apply { newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString()) putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
apply() } ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
} }
apply()
} }
val backupManager = BackupManager(this@PasswordActivity) val backupManager = BackupManager(this@PasswordActivity)
@@ -391,14 +397,18 @@ class PasswordActivity : StylishActivity() {
keyFile: Uri?, keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) { cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile verifyKeyFileCheckbox(keyFile)
loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity) loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
} }
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) { private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString()) val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile verifyKeyFileCheckbox(keyFile)
loadDatabase(password, keyFileUri) loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
} }
private fun removePassword() { private fun removePassword() {
@@ -406,7 +416,10 @@ class PasswordActivity : StylishActivity() {
checkboxPasswordView?.isChecked = false checkboxPasswordView?.isChecked = false
} }
private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) { private fun loadDatabase(databaseFileUri: Uri?,
password: String?,
keyFileUri: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
runOnUiThread { runOnUiThread {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) { if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
@@ -418,65 +431,119 @@ class PasswordActivity : StylishActivity() {
val database = Database.getInstance() val database = Database.getInstance()
database.closeAndClear(applicationContext.filesDir) database.closeAndClear(applicationContext.filesDir)
mDatabaseFileUri?.let { databaseUri -> databaseFileUri?.let { databaseUri ->
// Show the progress dialog and load the database
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
WeakReference(this@PasswordActivity),
database,
databaseUri,
password,
keyFile,
progressTaskUpdater,
AfterLoadingDatabase(database, password, cipherDatabaseEntity))
},
R.string.loading_database).start()
}
}
/** val onFinishLoadDatabase = object: ActionRunnable() {
* Called after verify and try to opening the database override fun onFinishRun(result: Result) {
*/ runOnUiThread {
private inner class AfterLoadingDatabase(val database: Database, val password: String?, // Recheck fingerprint if error
val cipherDatabaseEntity: CipherDatabaseEntity? = null) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
: ActionRunnable() { if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initBiometricMode()
}
}
override fun onFinishRun(result: Result) { if (result.isSuccess) {
runOnUiThread { // Save keyFile in app database
// Recheck fingerprint if error if (mRememberKeyFile) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mDatabaseFileUri?.let { databaseUri ->
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) { saveKeyFileData(databaseUri, mDatabaseKeyFileUri)
// Stay with the same mode and init it
advancedUnlockedManager?.initBiometricMode()
}
}
if (result.isSuccess) {
// Remove the password in view in all cases
removePassword()
// Register the biometric
if (cipherDatabaseEntity != null) {
CipherDatabaseAction.getInstance(this@PasswordActivity)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
checkAndLaunchGroupActivity(database, password)
} }
} else { }
checkAndLaunchGroupActivity(database, password)
}
} else { // Remove the password in view in all cases
if (result.message != null && result.message!!.isNotEmpty()) { removePassword()
Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show()
// Register the biometric
if (cipherDatabaseEntity != null) {
CipherDatabaseAction.getInstance(this@PasswordActivity)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
checkAndLaunchGroupActivity(database, password, keyFileUri)
}
} else {
checkAndLaunchGroupActivity(database, password, keyFileUri)
}
} else {
var resultError = ""
val resultException = result.exception
val resultMessage = result.message
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
if (resultException is LoadDatabaseDuplicateUuidException)
showLoadDatabaseDuplicateUuidMessage {
showProgressDialogAndLoadDatabase(database,
databaseUri,
password,
keyFileUri,
true,
this)
}
}
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError, resultException)
Snackbar.make(activity_password_coordinator_layout, resultError, Snackbar.LENGTH_LONG).asError().show()
}
} }
} }
} }
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(database,
databaseUri,
password,
keyFileUri,
false,
onFinishLoadDatabase)
} }
} }
private fun checkAndLaunchGroupActivity(database: Database, password: String?) { private fun showProgressDialogAndLoadDatabase(database: Database,
if (database.validatePasswordEncoding(password)) { databaseUri: Uri,
password: String?,
keyFile: Uri?,
fixDuplicateUUID: Boolean,
onFinishLoadDatabase: ActionRunnable) {
ProgressDialogThread(this,
{ progressTaskUpdater ->
LoadDatabaseRunnable(
database,
databaseUri,
password,
keyFile,
this@PasswordActivity.contentResolver,
this@PasswordActivity.filesDir,
SearchDbHelper(PreferencesUtil.omitBackup(this@PasswordActivity)),
fixDuplicateUUID,
progressTaskUpdater,
onFinishLoadDatabase)
},
R.string.loading_database).start()
}
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
DuplicateUuidDialog().apply {
positiveAction = loadDatabaseWithFix
}.show(supportFragmentManager, "duplicateUUIDDialog")
}
private fun saveKeyFileData(databaseUri: Uri, keyUri: Uri?) {
var keyFileUri = keyUri
if (!mRememberKeyFile) {
keyFileUri = null
}
FileDatabaseHistoryAction.getInstance(this).addOrUpdateDatabaseUri(databaseUri, keyFileUri)
}
private fun checkAndLaunchGroupActivity(database: Database, password: String?, keyFileUri: Uri?) {
if (database.validatePasswordEncoding(password, keyFileUri != null)) {
launchGroupActivity() launchGroupActivity()
} else { } else {
PasswordEncodingDialogFragment().apply { PasswordEncodingDialogFragment().apply {

View File

@@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var rootView: View? = null private var rootView: View? = null
private var passwordCheckBox: CompoundButton? = null private var passwordCheckBox: CompoundButton? = null
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null private var passwordView: TextView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null private var passwordRepeatView: TextView? = null
@@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
var allowNoMasterKey = false
arguments?.apply {
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
}
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater val inflater = activity.layoutInflater
@@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
.setTitle(R.string.assign_master_key) .setTitle(R.string.assign_master_key)
// Add action buttons // Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password) passwordView = rootView?.findViewById(R.id.pass_password)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password) passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
@@ -116,7 +126,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileView = rootView?.findViewById(R.id.pass_keyfile) keyFileView = rootView?.findViewById(R.id.pass_keyfile)
mOpenFileHelper = OpenFileHelper(this) mOpenFileHelper = OpenFileHelper(this)
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view -> rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) } mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
val dialog = builder.create() val dialog = builder.create()
@@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
var error = verifyPassword() || verifyFile() var error = verifyPassword() || verifyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true error = true
showNoKeyConfirmationDialog() if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
} }
if (!error) { if (!error) {
mListener?.onAssignKeyDialogPositiveClick( mListener?.onAssignKeyDialogPositiveClick(
@@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
showEmptyPasswordConfirmationDialog() showEmptyPasswordConfirmationDialog()
} }
} }
return error return error
} }
@@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
this@AssignMasterKeyDialogFragment.dismiss() this@AssignMasterKeyDialogFragment.dismiss()
} }
} }
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show() builder.create().show()
} }
} }
@@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox!!.isChecked, mKeyFile) keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss() this@AssignMasterKeyDialogFragment.dismiss()
} }
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show() builder.create().show()
} }
} }
@@ -255,4 +270,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
} }
} }
} }
companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
val fragment = AssignMasterKeyDialogFragment()
val args = Bundle()
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
fragment.arguments = args
return fragment
}
}
} }

View File

@@ -36,7 +36,7 @@ class BrowserDialogFragment : DialogFragment() {
// Get the layout inflater // Get the layout inflater
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null) val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
builder.setView(root) builder.setView(root)
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description) val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
textDescription.text = getString(R.string.file_manager_install_description) textDescription.text = getString(R.string.file_manager_install_description)

View File

@@ -0,0 +1,54 @@
/*
* 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.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
class DuplicateUuidDialog : DialogFragment() {
var positiveAction: (() -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
val message = getString(R.string.contains_duplicate_uuid) +
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
setMessage(message)
setPositiveButton(getString(android.R.string.ok)) { _, _ ->
positiveAction?.invoke()
dismiss()
}
setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() }
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
override fun onPause() {
super.onPause()
this.dismiss()
}
}

View File

@@ -114,7 +114,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
dismiss() dismiss()
} }
.setNegativeButton(R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
val bundle = Bundle() val bundle = Bundle()
mListener?.cancelPassword(bundle) mListener?.cancelPassword(bundle)

View File

@@ -122,7 +122,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.setView(root) builder.setView(root)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
editGroupListener?.cancelEditGroup( editGroupListener?.cancelEditGroup(
editGroupDialogAction, editGroupDialogAction,
nameTextView?.text?.toString(), nameTextView?.text?.toString(),

View File

@@ -77,7 +77,7 @@ class IconPickerDialogFragment : DialogFragment() {
dismiss() dismiss()
} }
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() } builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
return builder.create() return builder.create()
} }

View File

@@ -35,7 +35,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning) builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener) builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
return builder.create() return builder.create()
} }

View File

@@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() {
// Add action buttons // Add action buttons
.setPositiveButton(android.R.string.ok .setPositiveButton(android.R.string.ok
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) } ) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending) val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
// Check if is ascending or descending // Check if is ascending or descending

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.activities.helpers package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
@@ -26,10 +27,10 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -39,7 +40,7 @@ class OpenFileHelper {
private var fragment: Fragment? = null private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener(null) get() = OpenFileOnClickViewListener()
constructor(context: Activity) { constructor(context: Activity) {
this.activity = context this.activity = context
@@ -51,7 +52,7 @@ class OpenFileHelper {
this.fragment = context this.fragment = context
} }
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri?)?) : View.OnClickListener { inner class OpenFileOnClickViewListener : View.OnClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
try { try {
@@ -62,58 +63,44 @@ class OpenFileHelper {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Enable to start the file picker activity", e) Log.e(TAG, "Enable to start the file picker activity", e)
// Open browser dialog
// Open File picker if can't open activity if (lookForOpenIntentsFilePicker())
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
showBrowserDialog() showBrowserDialog()
} }
} }
} }
@SuppressLint("InlinedApi")
private fun openActivityWithActionOpenDocument() { private fun openActivityWithActionOpenDocument() {
val i = Intent(ACTION_OPEN_DOCUMENT) val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
i.addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*" type = "*/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
} else {
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
} }
if (fragment != null) if (fragment != null)
fragment?.startActivityForResult(i, OPEN_DOC) fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else else
activity?.startActivityForResult(i, OPEN_DOC) activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
} }
private fun openActivityWithActionGetContent() { private fun openActivityWithActionGetContent() {
val i = Intent(Intent.ACTION_GET_CONTENT) val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
i.addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*" type = "*/*"
}
if (fragment != null) if (fragment != null)
fragment?.startActivityForResult(i, GET_CONTENT) fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else else
activity?.startActivityForResult(i, GET_CONTENT) activity?.startActivityForResult(intentGetContent, GET_CONTENT)
} }
fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener { private fun lookForOpenIntentsFilePicker(): Boolean {
return OpenFileOnClickViewListener(dataUri)
}
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
var showBrowser = false var showBrowser = false
try { try {
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) { if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
val intent = Intent(OPEN_INTENTS_FILE_BROWSE) val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
// Get file path parent if possible
if (dataUri != null
&& dataUri.toString().isNotEmpty()
&& dataUri.scheme == "file") {
intent.data = dataUri
} else {
Log.w(javaClass.name, "Unable to read the URI")
}
if (fragment != null) if (fragment != null)
fragment?.startActivityForResult(intent, FILE_BROWSE) fragment?.startActivityForResult(intent, FILE_BROWSE)
else else
@@ -220,15 +207,10 @@ class OpenFileHelper {
private const val TAG = "OpenFileHelper" private const val TAG = "OpenFileHelper"
private var ACTION_OPEN_DOCUMENT: String private var APP_ACTION_OPEN_DOCUMENT: String = try {
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
init { } catch (e: Exception) {
ACTION_OPEN_DOCUMENT = try { "android.intent.action.OPEN_DOCUMENT"
val openDocument = Intent::class.java.getField("ACTION_OPEN_DOCUMENT")
openDocument.get(null) as String
} catch (e: Exception) {
"android.intent.action.OPEN_DOCUMENT"
}
} }
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE" const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"

View File

@@ -0,0 +1,50 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.EntryVersioned
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
}
override fun onBindViewHolder(holder: EntryHistoryViewHolder, position: Int) {
val entryHistory = entryHistoryList[position]
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
holder.titleView.text = entryHistory.title
holder.usernameView.text = entryHistory.username
holder.urlView.text = entryHistory.url
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryHistory, position)
}
}
override fun getItemCount(): Int {
return entryHistoryList.size
}
fun clear() {
entryHistoryList.clear()
}
inner class EntryHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
}
}

View File

@@ -19,7 +19,7 @@ interface FileDatabaseHistoryDao {
@Delete @Delete
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
@Query("REPLACE INTO file_database_history(keyfile_uri) VALUES(null)") @Query("UPDATE file_database_history SET keyfile_uri=null")
fun deleteAllKeyFiles() fun deleteAllKeyFiles()
@Query("DELETE FROM file_database_history") @Query("DELETE FROM file_database_history")

View File

@@ -32,12 +32,10 @@ class AesKdf internal constructor() : KdfEngine() {
override val defaultParameters: KdfParameters override val defaultParameters: KdfParameters
get() { get() {
val p = KdfParameters(uuid) return KdfParameters(uuid).apply {
setParamUUID()
p.setParamUUID() setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
p.setUInt32(ParamRounds, DEFAULT_ROUNDS.toLong()) }
return p
} }
override val defaultKeyRounds: Long override val defaultKeyRounds: Long
@@ -54,8 +52,8 @@ class AesKdf internal constructor() : KdfEngine() {
@Throws(IOException::class) @Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray { override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
var currentMasterKey = masterKey var currentMasterKey = masterKey
val rounds = p.getUInt64(ParamRounds) val rounds = p.getUInt64(PARAM_ROUNDS)
var seed = p.getByteArray(ParamSeed) var seed = p.getByteArray(PARAM_SEED)
if (currentMasterKey.size != 32) { if (currentMasterKey.size != 32) {
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey) currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
@@ -75,15 +73,15 @@ class AesKdf internal constructor() : KdfEngine() {
val seed = ByteArray(32) val seed = ByteArray(32)
random.nextBytes(seed) random.nextBytes(seed)
p.setByteArray(ParamSeed, seed) p.setByteArray(PARAM_SEED, seed)
} }
override fun getKeyRounds(p: KdfParameters): Long { override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(ParamRounds) return p.getUInt64(PARAM_ROUNDS)
} }
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) { override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(ParamRounds, keyRounds) p.setUInt64(PARAM_ROUNDS, keyRounds)
} }
companion object { companion object {
@@ -91,9 +89,24 @@ class AesKdf internal constructor() : KdfEngine() {
private const val DEFAULT_ROUNDS = 6000 private const val DEFAULT_ROUNDS = 6000
val CIPHER_UUID: UUID = Types.bytestoUUID( val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0xC9.toByte(), 0xD9.toByte(), 0xF3.toByte(), 0x9A.toByte(), 0x62.toByte(), 0x8A.toByte(), 0x44.toByte(), 0x60.toByte(), 0xBF.toByte(), 0x74.toByte(), 0x0D.toByte(), 0x08.toByte(), 0xC1.toByte(), 0x8A.toByte(), 0x4F.toByte(), 0xEA.toByte())) byteArrayOf(0xC9.toByte(),
0xD9.toByte(),
0xF3.toByte(),
0x9A.toByte(),
0x62.toByte(),
0x8A.toByte(),
0x44.toByte(),
0x60.toByte(),
0xBF.toByte(),
0x74.toByte(),
0x0D.toByte(),
0x08.toByte(),
0xC1.toByte(),
0x8A.toByte(),
0x4F.toByte(),
0xEA.toByte()))
const val ParamRounds = "R" const val PARAM_ROUNDS = "R"
const val ParamSeed = "S" const val PARAM_SEED = "S"
} }
} }

View File

@@ -33,16 +33,16 @@ class Argon2Kdf internal constructor() : KdfEngine() {
val p = KdfParameters(uuid) val p = KdfParameters(uuid)
p.setParamUUID() p.setParamUUID()
p.setUInt32(ParamParallelism, DefaultParallelism) p.setUInt32(PARAM_PARALLELISM, DEFAULT_PARALLELISM)
p.setUInt64(ParamMemory, DefaultMemory) p.setUInt64(PARAM_MEMORY, DEFAULT_MEMORY)
p.setUInt64(ParamIterations, DefaultIterations) p.setUInt64(PARAM_ITERATIONS, DEFAULT_ITERATIONS)
p.setUInt32(ParamVersion, MaxVersion) p.setUInt32(PARAM_VERSION, MAX_VERSION)
return p return p
} }
override val defaultKeyRounds: Long override val defaultKeyRounds: Long
get() = DefaultIterations get() = DEFAULT_ITERATIONS
init { init {
uuid = CIPHER_UUID uuid = CIPHER_UUID
@@ -55,13 +55,13 @@ class Argon2Kdf internal constructor() : KdfEngine() {
@Throws(IOException::class) @Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray { override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
val salt = p.getByteArray(ParamSalt) val salt = p.getByteArray(PARAM_SALT)
val parallelism = p.getUInt32(ParamParallelism).toInt() val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt()
val memory = p.getUInt64(ParamMemory) val memory = p.getUInt64(PARAM_MEMORY)
val iterations = p.getUInt64(ParamIterations) val iterations = p.getUInt64(PARAM_ITERATIONS)
val version = p.getUInt32(ParamVersion) val version = p.getUInt32(PARAM_VERSION)
val secretKey = p.getByteArray(ParamSecretKey) val secretKey = p.getByteArray(PARAM_SECRET_KEY)
val assocData = p.getByteArray(ParamAssocData) val assocData = p.getByteArray(PARAM_ASSOC_DATA)
return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations, return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations,
secretKey, assocData, version) secretKey, assocData, version)
@@ -73,71 +73,102 @@ class Argon2Kdf internal constructor() : KdfEngine() {
val salt = ByteArray(32) val salt = ByteArray(32)
random.nextBytes(salt) random.nextBytes(salt)
p.setByteArray(ParamSalt, salt) p.setByteArray(PARAM_SALT, salt)
} }
override fun getKeyRounds(p: KdfParameters): Long { override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(ParamIterations) return p.getUInt64(PARAM_ITERATIONS)
} }
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) { override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(ParamIterations, keyRounds) p.setUInt64(PARAM_ITERATIONS, keyRounds)
} }
override val minKeyRounds: Long
get() = MIN_ITERATIONS
override val maxKeyRounds: Long
get() = MAX_ITERATIONS
override fun getMemoryUsage(p: KdfParameters): Long { override fun getMemoryUsage(p: KdfParameters): Long {
return p.getUInt64(ParamMemory) return p.getUInt64(PARAM_MEMORY)
} }
override fun setMemoryUsage(p: KdfParameters, memory: Long) { override fun setMemoryUsage(p: KdfParameters, memory: Long) {
p.setUInt64(ParamMemory, memory) p.setUInt64(PARAM_MEMORY, memory)
} }
override fun getDefaultMemoryUsage(): Long { override val defaultMemoryUsage: Long
return DefaultMemory get() = DEFAULT_MEMORY
}
override val minMemoryUsage: Long
get() = MIN_MEMORY
override val maxMemoryUsage: Long
get() = MAX_MEMORY
override fun getParallelism(p: KdfParameters): Int { override fun getParallelism(p: KdfParameters): Int {
return p.getUInt32(ParamParallelism).toInt() // TODO Verify return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify
} }
override fun setParallelism(p: KdfParameters, parallelism: Int) { override fun setParallelism(p: KdfParameters, parallelism: Int) {
p.setUInt32(ParamParallelism, parallelism.toLong()) p.setUInt32(PARAM_PARALLELISM, parallelism.toLong())
} }
override fun getDefaultParallelism(): Int { override val defaultParallelism: Int
return DefaultParallelism.toInt() // TODO Verify get() = DEFAULT_PARALLELISM.toInt()
}
override val minParallelism: Int
get() = MIN_PARALLELISM
override val maxParallelism: Int
get() = MAX_PARALLELISM
companion object { companion object {
val CIPHER_UUID: UUID = Types.bytestoUUID( val CIPHER_UUID: UUID = Types.bytestoUUID(
byteArrayOf(0xEF.toByte(), 0x63.toByte(), 0x6D.toByte(), 0xDF.toByte(), 0x8C.toByte(), 0x29.toByte(), 0x44.toByte(), 0x4B.toByte(), 0x91.toByte(), 0xF7.toByte(), 0xA9.toByte(), 0xA4.toByte(), 0x03.toByte(), 0xE3.toByte(), 0x0A.toByte(), 0x0C.toByte())) byteArrayOf(0xEF.toByte(),
0x63.toByte(),
0x6D.toByte(),
0xDF.toByte(),
0x8C.toByte(),
0x29.toByte(),
0x44.toByte(),
0x4B.toByte(),
0x91.toByte(),
0xF7.toByte(),
0xA9.toByte(),
0xA4.toByte(),
0x03.toByte(),
0xE3.toByte(),
0x0A.toByte(),
0x0C.toByte()))
private const val ParamSalt = "S" // byte[] private const val PARAM_SALT = "S" // byte[]
private const val ParamParallelism = "P" // UInt32 private const val PARAM_PARALLELISM = "P" // UInt32
private const val ParamMemory = "M" // UInt64 private const val PARAM_MEMORY = "M" // UInt64
private const val ParamIterations = "I" // UInt64 private const val PARAM_ITERATIONS = "I" // UInt64
private const val ParamVersion = "V" // UInt32 private const val PARAM_VERSION = "V" // UInt32
private const val ParamSecretKey = "K" // byte[] private const val PARAM_SECRET_KEY = "K" // byte[]
private const val ParamAssocData = "A" // byte[] private const val PARAM_ASSOC_DATA = "A" // byte[]
private const val MinVersion: Long = 0x10 private const val MIN_VERSION: Long = 0x10
private const val MaxVersion: Long = 0x13 private const val MAX_VERSION: Long = 0x13
private const val MinSalt = 8 private const val MIN_SALT = 8
private const val MaxSalt = Integer.MAX_VALUE private const val MAX_SALT = Integer.MAX_VALUE
private const val MinIterations: Long = 1 private const val MIN_ITERATIONS: Long = 1
private const val MaxIterations = 4294967295L private const val MAX_ITERATIONS = 4294967295L
private const val MinMemory = (1024 * 8).toLong() private const val MIN_MEMORY = (1024 * 8).toLong()
private const val MaxMemory = Integer.MAX_VALUE.toLong() private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
private const val MinParallelism = 1 private const val MIN_PARALLELISM = 1
private const val MaxParallelism = (1 shl 24) - 1 private const val MAX_PARALLELISM = (1 shl 24) - 1
private const val DefaultIterations: Long = 2 private const val DEFAULT_ITERATIONS: Long = 2
private const val DefaultMemory = (1024 * 1024).toLong() private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
private const val DefaultParallelism: Long = 2 private const val DEFAULT_PARALLELISM: Long = 2
} }
} }

View File

@@ -30,17 +30,31 @@ abstract class KdfEngine : ObjectNameResource {
abstract val defaultParameters: KdfParameters abstract val defaultParameters: KdfParameters
abstract val defaultKeyRounds: Long
@Throws(IOException::class) @Throws(IOException::class)
abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray
abstract fun randomize(p: KdfParameters) abstract fun randomize(p: KdfParameters)
/*
* ITERATIONS
*/
abstract fun getKeyRounds(p: KdfParameters): Long abstract fun getKeyRounds(p: KdfParameters): Long
abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long) abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long)
abstract val defaultKeyRounds: Long
open val minKeyRounds: Long
get() = 1
open val maxKeyRounds: Long
get() = Int.MAX_VALUE.toLong()
/*
* MEMORY
*/
open fun getMemoryUsage(p: KdfParameters): Long { open fun getMemoryUsage(p: KdfParameters): Long {
return UNKNOWN_VALUE.toLong() return UNKNOWN_VALUE.toLong()
} }
@@ -49,9 +63,18 @@ abstract class KdfEngine : ObjectNameResource {
// Do nothing by default // Do nothing by default
} }
open fun getDefaultMemoryUsage(): Long { open val defaultMemoryUsage: Long
return UNKNOWN_VALUE.toLong() get() = UNKNOWN_VALUE.toLong()
}
open val minMemoryUsage: Long
get() = 1
open val maxMemoryUsage: Long
get() = Int.MAX_VALUE.toLong()
/*
* PARALLELISM
*/
open fun getParallelism(p: KdfParameters): Int { open fun getParallelism(p: KdfParameters): Int {
return UNKNOWN_VALUE return UNKNOWN_VALUE
@@ -61,13 +84,16 @@ abstract class KdfEngine : ObjectNameResource {
// Do nothing by default // Do nothing by default
} }
open fun getDefaultParallelism(): Int { open val defaultParallelism: Int
return UNKNOWN_VALUE get() = UNKNOWN_VALUE
}
open val minParallelism: Int
get() = 1
open val maxParallelism: Int
get() = Int.MAX_VALUE
companion object { companion object {
const val UNKNOWN_VALUE = -1 const val UNKNOWN_VALUE = -1
const val UNKNOWN_VALUE_STRING = (-1).toString()
} }
} }

View File

@@ -19,37 +19,7 @@
*/ */
package com.kunzisoft.keepass.crypto.keyDerivation package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.database.exception.UnknownKDF
import java.util.ArrayList
object KdfFactory { object KdfFactory {
var aesKdf = AesKdf() var aesKdf = AesKdf()
var argon2Kdf = Argon2Kdf() var argon2Kdf = Argon2Kdf()
var kdfListV3: MutableList<KdfEngine> = ArrayList()
var kdfListV4: MutableList<KdfEngine> = ArrayList()
init {
kdfListV3.add(aesKdf)
kdfListV4.add(aesKdf)
kdfListV4.add(argon2Kdf)
}
@Throws(UnknownKDF::class)
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
val unknownKDFException = UnknownKDF()
if (kdfParameters == null) {
throw unknownKDFException
}
for (engine in kdfListV4) {
if (engine.uuid == kdfParameters.uuid) {
return engine
}
}
throw unknownKDFException
}
} }

View File

@@ -140,7 +140,7 @@ enum class SortNodeEnum {
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
return object1.creationTime.date return object1.creationTime.date
?.compareTo(object2.creationTime.date) ?: 0 .compareTo(object2.creationTime.date)
} }
} }
@@ -152,7 +152,7 @@ enum class SortNodeEnum {
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
return object1.lastModificationTime.date return object1.lastModificationTime.date
?.compareTo(object2.lastModificationTime.date) ?: 0 .compareTo(object2.lastModificationTime.date)
} }
} }
@@ -164,7 +164,7 @@ enum class SortNodeEnum {
override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int { override fun compareBySpecificOrder(object1: NodeVersioned, object2: NodeVersioned): Int {
return object1.lastAccessTime.date return object1.lastAccessTime.date
?.compareTo(object2.lastAccessTime.date) ?: 0 .compareTo(object2.lastAccessTime.date)
} }
} }
} }

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import java.io.IOException import java.io.IOException
@@ -63,7 +63,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
// To save the database // To save the database
super.run() super.run()
finishRun(true) finishRun(true)
} catch (e: InvalidKeyFileException) { } catch (e: LoadDatabaseInvalidKeyFileException) {
erase(mBackupKey) erase(mBackupKey)
finishRun(false, e.message) finishRun(false, e.message)
} catch (e: IOException) { } catch (e: IOException) {

View File

@@ -19,118 +19,45 @@
*/ */
package com.kunzisoft.keepass.database.action package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.preference.PreferenceManager
import androidx.annotation.StringRes
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.search.SearchDbHelper
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.FileNotFoundException import java.io.File
import java.io.IOException
import java.lang.ref.WeakReference
class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>, class LoadDatabaseRunnable(private val mDatabase: Database,
private val mDatabase: Database,
private val mUri: Uri, private val mUri: Uri,
private val mPass: String?, private val mPass: String?,
private val mKey: Uri?, private val mKey: Uri?,
private val contentResolver: ContentResolver,
private val cacheDirectory: File,
private val mSearchHelper: SearchDbHelper,
private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?, private val progressTaskUpdater: ProgressTaskUpdater?,
nestedAction: ActionRunnable) nestedAction: ActionRunnable)
: ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) { : ActionRunnable(nestedAction, executeNestedActionIfResultFalse = true) {
private val mRememberKeyFile: Boolean
get() {
return mWeakContext.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it)
.getBoolean(it.getString(R.string.keyfile_key),
it.resources.getBoolean(R.bool.keyfile_default))
} ?: true
}
override fun run() { override fun run() {
try { try {
mWeakContext.get()?.let { mDatabase.loadData(mUri, mPass, mKey,
mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater) contentResolver,
saveFileData(mUri, mKey) cacheDirectory,
finishRun(true) mSearchHelper,
} ?: finishRun(false, "Context null") mFixDuplicateUUID,
} catch (e: ArcFourException) { progressTaskUpdater)
catchError(e, R.string.error_arc4) finishRun(true)
return
} catch (e: InvalidPasswordException) {
catchError(e, R.string.invalid_password)
return
} catch (e: ContentFileNotFoundException) {
catchError(e, R.string.file_not_found_content)
return
} catch (e: FileNotFoundException) {
catchError(e, R.string.file_not_found)
return
} catch (e: IOException) {
var messageId = R.string.error_load_database
e.message?.let {
if (it.contains("Hash failed with code"))
messageId = R.string.error_load_database_KDF_memory
}
catchError(e, messageId, true)
return
} catch (e: KeyFileEmptyException) {
catchError(e, R.string.keyfile_is_empty)
return
} catch (e: InvalidAlgorithmException) {
catchError(e, R.string.invalid_algorithm)
return
} catch (e: InvalidKeyFileException) {
catchError(e, R.string.keyfile_does_not_exist)
return
} catch (e: InvalidDBSignatureException) {
catchError(e, R.string.invalid_db_sig)
return
} catch (e: InvalidDBVersionException) {
catchError(e, R.string.unsupported_db_version)
return
} catch (e: InvalidDBException) {
catchError(e, R.string.error_invalid_db)
return
} catch (e: OutOfMemoryError) {
catchError(e, R.string.error_out_of_memory)
return
} catch (e: Exception) {
catchError(e, R.string.error_load_database, true)
return
} }
} catch (e: LoadDatabaseException) {
finishRun(false, e)
private fun catchError(e: Throwable, @StringRes messageId: Int, addThrowableMessage: Boolean = false) {
var errorMessage = mWeakContext.get()?.getString(messageId)
Log.e(TAG, errorMessage, e)
if (addThrowableMessage)
errorMessage = errorMessage + " " + e.localizedMessage
finishRun(false, errorMessage)
}
private fun saveFileData(uri: Uri, key: Uri?) {
var keyFileUri = key
if (!mRememberKeyFile) {
keyFileUri = null
}
mWeakContext.get()?.let {
FileDatabaseHistoryAction.getInstance(it).addOrUpdateDatabaseUri(uri, keyFileUri)
} }
} }
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
if (!result.isSuccess) { if (!result.isSuccess) {
mDatabase.closeAndClear(mWeakContext.get()?.filesDir) mDatabase.closeAndClear(cacheDirectory)
} }
} }
companion object {
private val TAG = LoadDatabaseRunnable::class.java.name
}
} }

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.PwDbOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import java.io.IOException import java.io.IOException
@@ -37,7 +37,7 @@ abstract class SaveDatabaseRunnable(protected var context: Context,
database.saveData(context.contentResolver) database.saveData(context.contentResolver)
} catch (e: IOException) { } catch (e: IOException) {
finishRun(false, e.message) finishRun(false, e.message)
} catch (e: PwDbOutputException) { } catch (e: DatabaseOutputException) {
finishRun(false, e.message) finishRun(false, e.message)
} }
} }

View File

@@ -74,6 +74,8 @@ class DeleteEntryRunnable constructor(
result.data = Bundle().apply { result.data = Bundle().apply {
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position ) putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
} }
} ?: run {
result.data?.remove(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
} }
// Return a copy of unchanged entry as old param // Return a copy of unchanged entry as old param

View File

@@ -73,6 +73,8 @@ class DeleteGroupRunnable(context: FragmentActivity,
result.data = Bundle().apply { result.data = Bundle().apply {
putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position ) putInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY, position )
} }
} ?: run {
result.data?.remove(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
} }
// Return a copy of unchanged group as old param // Return a copy of unchanged group as old param

View File

@@ -33,11 +33,17 @@ class UpdateEntryRunnable constructor(
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) { : ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
// Keep backup of original values in case save fails // Keep backup of original values in case save fails
private var mBackupEntry: EntryVersioned? = null private var mBackupEntryHistory: EntryVersioned? = null
override fun nodeAction() { override fun nodeAction() {
mBackupEntry = database.addHistoryBackupTo(mOldEntry) mNewEntry.touch(modified = true, touchParents = true)
mOldEntry.touch(modified = true, touchParents = true)
mBackupEntryHistory = EntryVersioned(mOldEntry)
// Create an entry history (an entry history don't have history)
mNewEntry.addEntryToHistory(EntryVersioned(mOldEntry, copyHistory = false))
database.removeOldestHistory(mNewEntry)
// Update entry with new values // Update entry with new values
mOldEntry.updateWith(mNewEntry) mOldEntry.updateWith(mNewEntry)
} }
@@ -45,7 +51,7 @@ class UpdateEntryRunnable constructor(
override fun nodeFinish(result: Result): ActionNodeValues { override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) { if (!result.isSuccess) {
// If we fail to save, back out changes to global structure // If we fail to save, back out changes to global structure
mBackupEntry?.let { mBackupEntryHistory?.let {
mOldEntry.updateWith(it) mOldEntry.updateWith(it)
} }
} }

View File

@@ -20,14 +20,12 @@
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.webkit.URLUtil import android.webkit.URLUtil
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.database.NodeHandler import com.kunzisoft.keepass.database.NodeHandler
import com.kunzisoft.keepass.database.cursor.EntryCursorV3 import com.kunzisoft.keepass.database.cursor.EntryCursorV3
import com.kunzisoft.keepass.database.cursor.EntryCursorV4 import com.kunzisoft.keepass.database.cursor.EntryCursorV4
@@ -40,7 +38,6 @@ import com.kunzisoft.keepass.database.file.save.PwDbV3Output
import com.kunzisoft.keepass.database.file.save.PwDbV4Output import com.kunzisoft.keepass.database.file.save.PwDbV4Output
import com.kunzisoft.keepass.database.search.SearchDbHelper import com.kunzisoft.keepass.database.search.SearchDbHelper
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.stream.LEDataInputStream import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.SingletonHolder
@@ -57,7 +54,8 @@ class Database {
private var pwDatabaseV4: PwDatabaseV4? = null private var pwDatabaseV4: PwDatabaseV4? = null
private var mUri: Uri? = null private var mUri: Uri? = null
private var searchHelper: SearchDbHelper? = null private var mSearchHelper: SearchDbHelper? = null
var isReadOnly = false var isReadOnly = false
val drawFactory = IconDrawableFactory() val drawFactory = IconDrawableFactory()
@@ -69,15 +67,23 @@ class Database {
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory() return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
} }
val name: String var name: String
get() { get() {
return pwDatabaseV4?.name ?: "" return pwDatabaseV4?.name ?: ""
} }
set(name) {
pwDatabaseV4?.name = name
pwDatabaseV4?.nameChanged = PwDate()
}
val description: String var description: String
get() { get() {
return pwDatabaseV4?.description ?: "" return pwDatabaseV4?.description ?: ""
} }
set(description) {
pwDatabaseV4?.description = description
pwDatabaseV4?.descriptionChanged = PwDate()
}
var defaultUsername: String var defaultUsername: String
get() { get() {
@@ -88,33 +94,56 @@ class Database {
pwDatabaseV4?.defaultUserNameChanged = PwDate() pwDatabaseV4?.defaultUserNameChanged = PwDate()
} }
val encryptionAlgorithm: PwEncryptionAlgorithm? // with format "#000000"
var color: String
get() { get() {
return pwDatabaseV4?.encryptionAlgorithm return pwDatabaseV4?.color ?: ""
}
set(value) {
// TODO Check color string
pwDatabaseV4?.color = value
}
val availableCompressionAlgorithms: List<PwCompressionAlgorithm>
get() = pwDatabaseV4?.availableCompressionAlgorithms ?: ArrayList()
var compressionAlgorithm: PwCompressionAlgorithm?
get() = pwDatabaseV4?.compressionAlgorithm
set(value) {
value?.let {
pwDatabaseV4?.compressionAlgorithm = it
}
// TODO Compression
} }
val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm> val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList() get() = pwDatabaseV3?.availableEncryptionAlgorithms ?: pwDatabaseV4?.availableEncryptionAlgorithms ?: ArrayList()
var encryptionAlgorithm: PwEncryptionAlgorithm?
get() = pwDatabaseV3?.encryptionAlgorithm ?: pwDatabaseV4?.encryptionAlgorithm
set(algorithm) {
algorithm?.let {
pwDatabaseV4?.encryptionAlgorithm = algorithm
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
pwDatabaseV4?.dataCipher = algorithm.dataCipher
}
}
val availableKdfEngines: List<KdfEngine> val availableKdfEngines: List<KdfEngine>
get() { get() = pwDatabaseV3?.kdfAvailableList ?: pwDatabaseV4?.kdfAvailableList ?: ArrayList()
if (pwDatabaseV3 != null) {
return KdfFactory.kdfListV3
}
if (pwDatabaseV4 != null) {
return KdfFactory.kdfListV4
}
return ArrayList()
}
val kdfEngine: KdfEngine var kdfEngine: KdfEngine?
get() { get() = pwDatabaseV3?.kdfEngine ?: pwDatabaseV4?.kdfEngine
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf set(kdfEngine) {
kdfEngine?.let {
if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
memoryUsage = kdfEngine.defaultMemoryUsage
parallelism = kdfEngine.defaultParallelism
}
} }
val numberKeyEncryptionRoundsAsString: String
get() = numberKeyEncryptionRounds.toString()
var numberKeyEncryptionRounds: Long var numberKeyEncryptionRounds: Long
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0 get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
@Throws(NumberFormatException::class) @Throws(NumberFormatException::class)
@@ -123,9 +152,6 @@ class Database {
pwDatabaseV4?.numberKeyEncryptionRounds = numberRounds pwDatabaseV4?.numberKeyEncryptionRounds = numberRounds
} }
val memoryUsageAsString: String
get() = memoryUsage.toString()
var memoryUsage: Long var memoryUsage: Long
get() { get() {
return pwDatabaseV4?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong() return pwDatabaseV4?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong()
@@ -134,9 +160,6 @@ class Database {
pwDatabaseV4?.memoryUsage = memory pwDatabaseV4?.memoryUsage = memory
} }
val parallelismAsString: String
get() = parallelism.toString()
var parallelism: Int var parallelism: Int
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOWN_VALUE get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOWN_VALUE
set(parallelism) { set(parallelism) {
@@ -161,6 +184,25 @@ class Database {
return null return null
} }
val manageHistory: Boolean
get() = pwDatabaseV4 != null
var historyMaxItems: Int
get() {
return pwDatabaseV4?.historyMaxItems ?: 0
}
set(value) {
pwDatabaseV4?.historyMaxItems = value
}
var historyMaxSize: Long
get() {
return pwDatabaseV4?.historyMaxSize ?: 0
}
set(value) {
pwDatabaseV4?.historyMaxSize = value
}
/** /**
* Determine if RecycleBin is available or not for this version of database * Determine if RecycleBin is available or not for this version of database
* @return true if RecycleBin available * @return true if RecycleBin available
@@ -206,78 +248,95 @@ class Database {
this.mUri = databaseUri this.mUri = databaseUri
} }
@Throws(IOException::class, InvalidDBException::class) @Throws(LoadDatabaseException::class)
fun loadData(ctx: Context, uri: Uri, password: String?, keyfile: Uri?, progressTaskUpdater: ProgressTaskUpdater?) { fun loadData(uri: Uri, password: String?, keyfile: Uri?,
contentResolver: ContentResolver,
cacheDirectory: File,
searchHelper: SearchDbHelper,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
mUri = uri
isReadOnly = false
if (uri.scheme == "file") {
val file = File(uri.path!!)
isReadOnly = !file.canWrite()
}
// Pass Uris as InputStreams
val inputStream: InputStream?
try { try {
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw ContentFileNotFoundException.getInstance(uri)
}
// Pass KeyFile Uri as InputStreams mUri = uri
var keyFileInputStream: InputStream? = null isReadOnly = false
keyfile?.let { if (uri.scheme == "file") {
val file = File(uri.path!!)
isReadOnly = !file.canWrite()
}
// Pass Uris as InputStreams
val inputStream: InputStream?
try { try {
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile) inputStream = UriUtil.getUriInputStream(contentResolver, uri)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KPD", "Database::loadData", e) Log.e("KPD", "Database::loadData", e)
throw ContentFileNotFoundException.getInstance(keyfile) throw LoadDatabaseFileNotFoundException()
} }
}
// Load Data // Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
keyfile?.let {
try {
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw LoadDatabaseFileNotFoundException()
}
}
val bufferedInputStream = BufferedInputStream(inputStream) // Load Data
if (!bufferedInputStream.markSupported()) {
throw IOException("Input stream does not support mark.")
}
// We'll end up reading 8 bytes to identify the header. Might as well use two extra. val bufferedInputStream = BufferedInputStream(inputStream)
bufferedInputStream.mark(10) if (!bufferedInputStream.markSupported()) {
throw IOException("Input stream does not support mark.")
}
// Get the file directory to save the attachments // We'll end up reading 8 bytes to identify the header. Might as well use two extra.
val sig1 = LEDataInputStream.readInt(bufferedInputStream) bufferedInputStream.mark(10)
val sig2 = LEDataInputStream.readInt(bufferedInputStream)
// Return to the start // Get the file directory to save the attachments
bufferedInputStream.reset() val sig1 = LEDataInputStream.readInt(bufferedInputStream)
val sig2 = LEDataInputStream.readInt(bufferedInputStream)
when { // Return to the start
// Header of database V3 bufferedInputStream.reset()
PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3()
.openDatabase(bufferedInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
// Header of database V4 when {
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(ctx.filesDir) // Header of database V3
.openDatabase(bufferedInputStream, PwDbHeaderV3.matchesHeader(sig1, sig2) -> setDatabaseV3(ImporterV3()
password, .openDatabase(bufferedInputStream,
keyFileInputStream, password,
progressTaskUpdater)) keyFileInputStream,
progressTaskUpdater))
// Header not recognized // Header of database V4
else -> throw InvalidDBSignatureException() PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(
} cacheDirectory,
fixDuplicateUUID)
.openDatabase(bufferedInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
try { // Header not recognized
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx)) else -> throw LoadDatabaseSignatureException()
}
this.mSearchHelper = searchHelper
loaded = true loaded = true
} catch (e: LoadDatabaseException) {
throw e
} catch (e: IOException) {
if (e.message?.contains("Hash failed with code") == true)
throw LoadDatabaseKDFMemoryException(e)
else
throw LoadDatabaseIOException(e)
} catch (e: OutOfMemoryError) {
throw LoadDatabaseNoMemoryException(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Load can't be performed with this Database version", e) throw LoadDatabaseException(e)
loaded = false
} }
} }
@@ -289,7 +348,7 @@ class Database {
@JvmOverloads @JvmOverloads
fun search(str: String, max: Int = Integer.MAX_VALUE): GroupVersioned? { fun search(str: String, max: Int = Integer.MAX_VALUE): GroupVersioned? {
return searchHelper?.search(this, str, max) return mSearchHelper?.search(this, str, max)
} }
fun searchEntries(query: String): Cursor? { fun searchEntries(query: String): Cursor? {
@@ -340,14 +399,14 @@ class Database {
return entry return entry
} }
@Throws(IOException::class, PwDbOutputException::class) @Throws(IOException::class, DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver) { fun saveData(contentResolver: ContentResolver) {
mUri?.let { mUri?.let {
saveData(contentResolver, it) saveData(contentResolver, it)
} }
} }
@Throws(IOException::class, PwDbOutputException::class) @Throws(IOException::class, DatabaseOutputException::class)
private fun saveData(contentResolver: ContentResolver, uri: Uri) { private fun saveData(contentResolver: ContentResolver, uri: Uri) {
val errorMessage = "Failed to store database." val errorMessage = "Failed to store database."
@@ -425,31 +484,30 @@ class Database {
return false return false
} }
fun assignName(name: String) {
pwDatabaseV4?.name = name
pwDatabaseV4?.nameChanged = PwDate()
}
fun containsDescription(): Boolean { fun containsDescription(): Boolean {
pwDatabaseV4?.let { return true } pwDatabaseV4?.let { return true }
return false return false
} }
fun assignDescription(description: String) { fun containsDefaultUsername(): Boolean {
pwDatabaseV4?.description = description pwDatabaseV4?.let { return true }
pwDatabaseV4?.descriptionChanged = PwDate() return false
}
fun containsCustomColor(): Boolean {
pwDatabaseV4?.let { return true }
return false
}
fun allowNoMasterKey(): Boolean {
pwDatabaseV4?.let { return true }
return false
} }
fun allowEncryptionAlgorithmModification(): Boolean { fun allowEncryptionAlgorithmModification(): Boolean {
return availableEncryptionAlgorithms.size > 1 return availableEncryptionAlgorithms.size > 1
} }
fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) {
pwDatabaseV4?.encryptionAlgorithm = algorithm
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
pwDatabaseV4?.dataCipher = algorithm.dataCipher
}
fun getEncryptionAlgorithmName(resources: Resources): String { fun getEncryptionAlgorithmName(resources: Resources): String {
return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) ?: "" return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) ?: ""
} }
@@ -458,25 +516,17 @@ class Database {
return availableKdfEngines.size > 1 return availableKdfEngines.size > 1
} }
fun assignKdfEngine(kdfEngine: KdfEngine) {
if (pwDatabaseV4?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
pwDatabaseV4?.kdfParameters = kdfEngine.defaultParameters
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
memoryUsage = kdfEngine.getDefaultMemoryUsage()
parallelism = kdfEngine.getDefaultParallelism()
}
fun getKeyDerivationName(resources: Resources): String { fun getKeyDerivationName(resources: Resources): String {
return kdfEngine.getName(resources) return kdfEngine?.getName(resources) ?: ""
} }
fun validatePasswordEncoding(key: String?): Boolean { fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
return pwDatabaseV3?.validatePasswordEncoding(key) return pwDatabaseV3?.validatePasswordEncoding(password, containsKeyFile)
?: pwDatabaseV4?.validatePasswordEncoding(key) ?: pwDatabaseV4?.validatePasswordEncoding(password, containsKeyFile)
?: false ?: false
} }
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
pwDatabaseV3?.retrieveMasterKey(key, keyInputStream) pwDatabaseV3?.retrieveMasterKey(key, keyInputStream)
pwDatabaseV4?.retrieveMasterKey(key, keyInputStream) pwDatabaseV4?.retrieveMasterKey(key, keyInputStream)
@@ -516,7 +566,7 @@ class Database {
return null return null
} }
fun getEntryById(id: PwNodeId<*>): EntryVersioned? { fun getEntryById(id: PwNodeId<UUID>): EntryVersioned? {
pwDatabaseV3?.getEntryById(id)?.let { pwDatabaseV3?.getEntryById(id)?.let {
return EntryVersioned(it) return EntryVersioned(it)
} }
@@ -527,12 +577,14 @@ class Database {
} }
fun getGroupById(id: PwNodeId<*>): GroupVersioned? { fun getGroupById(id: PwNodeId<*>): GroupVersioned? {
pwDatabaseV3?.getGroupById(id)?.let { if (id is PwNodeIdInt)
return GroupVersioned(it) pwDatabaseV3?.getGroupById(id)?.let {
} return GroupVersioned(it)
pwDatabaseV4?.getGroupById(id)?.let { }
return GroupVersioned(it) else if (id is PwNodeIdUUID)
} pwDatabaseV4?.getGroupById(id)?.let {
return GroupVersioned(it)
}
return null return null
} }
@@ -702,31 +754,28 @@ class Database {
} }
} }
fun addHistoryBackupTo(entry: EntryVersioned): EntryVersioned { fun removeOldestHistory(entry: EntryVersioned) {
val backupEntry = EntryVersioned(entry)
entry.addBackupToHistory() // Remove oldest history if more than max items or max memory
// Remove oldest backup if more than max items or max memory
pwDatabaseV4?.let { pwDatabaseV4?.let {
val history = entry.getHistory() val history = entry.getHistory()
val maxItems = it.historyMaxItems val maxItems = historyMaxItems
if (maxItems >= 0) { if (maxItems >= 0) {
while (history.size > maxItems) { while (history.size > maxItems) {
entry.removeOldestEntryFromHistory() entry.removeOldestEntryFromHistory()
} }
} }
val maxSize = it.historyMaxSize val maxSize = historyMaxSize
if (maxSize >= 0) { if (maxSize >= 0) {
while (true) { while (true) {
var histSize: Long = 0 var historySize: Long = 0
for (backup in history) { for (entryHistory in history) {
histSize += backup.size historySize += entryHistory.getSize()
} }
if (histSize > maxSize) { if (historySize > maxSize) {
entry.removeOldestEntryFromHistory() entry.removeOldestEntryFromHistory()
} else { } else {
break break
@@ -734,8 +783,6 @@ class Database {
} }
} }
} }
return backupEntry
} }
companion object : SingletonHolder<Database>(::Database) { companion object : SingletonHolder<Database>(::Database) {

View File

@@ -15,26 +15,26 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
var pwEntryV4: PwEntryV4? = null var pwEntryV4: PwEntryV4? = null
private set private set
fun updateWith(entry: EntryVersioned) { fun updateWith(entry: EntryVersioned, copyHistory: Boolean = true) {
entry.pwEntryV3?.let { entry.pwEntryV3?.let {
this.pwEntryV3?.updateWith(it) this.pwEntryV3?.updateWith(it)
} }
entry.pwEntryV4?.let { entry.pwEntryV4?.let {
this.pwEntryV4?.updateWith(it) this.pwEntryV4?.updateWith(it, copyHistory)
} }
} }
/** /**
* Use this constructor to copy an Entry with exact same values * Use this constructor to copy an Entry with exact same values
*/ */
constructor(entry: EntryVersioned) { constructor(entry: EntryVersioned, copyHistory: Boolean = true) {
if (entry.pwEntryV3 != null) { if (entry.pwEntryV3 != null) {
this.pwEntryV3 = PwEntryV3() this.pwEntryV3 = PwEntryV3()
} }
if (entry.pwEntryV4 != null) { if (entry.pwEntryV4 != null) {
this.pwEntryV4 = PwEntryV4() this.pwEntryV4 = PwEntryV4()
} }
updateWith(entry) updateWith(entry, copyHistory)
} }
constructor(entry: PwEntryV3) { constructor(entry: PwEntryV3) {
@@ -241,6 +241,10 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
return pwEntryV4?.allowCustomFields() ?: false return pwEntryV4?.allowCustomFields() ?: false
} }
fun removeAllFields() {
pwEntryV4?.removeAllFields()
}
/** /**
* Add an extra field to the list (standard or custom) * Add an extra field to the list (standard or custom)
* @param label Label of field, must be unique * @param label Label of field, must be unique
@@ -258,20 +262,31 @@ class EntryVersioned : NodeVersioned, PwEntryInterface<GroupVersioned> {
pwEntryV4?.stopToManageFieldReferences() pwEntryV4?.stopToManageFieldReferences()
} }
fun addBackupToHistory() { fun getHistory(): ArrayList<EntryVersioned> {
pwEntryV4?.let { val history = ArrayList<EntryVersioned>()
val entryHistory = PwEntryV4() val entryV4History = pwEntryV4?.history ?: ArrayList()
entryHistory.updateWith(it) for (entryHistory in entryV4History) {
it.addEntryToHistory(entryHistory) history.add(EntryVersioned(entryHistory))
} }
return history
}
fun addEntryToHistory(entry: EntryVersioned) {
entry.pwEntryV4?.let {
pwEntryV4?.addEntryToHistory(it)
}
}
fun removeAllHistory() {
pwEntryV4?.removeAllHistory()
} }
fun removeOldestEntryFromHistory() { fun removeOldestEntryFromHistory() {
pwEntryV4?.removeOldestEntryFromHistory() pwEntryV4?.removeOldestEntryFromHistory()
} }
fun getHistory(): ArrayList<PwEntryV4> { fun getSize(): Long {
return pwEntryV4?.history ?: ArrayList() return pwEntryV4?.size ?: 0L
} }
fun containsCustomData(): Boolean { fun containsCustomData(): Boolean {

View File

@@ -17,25 +17,24 @@
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>. * along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.database.file package com.kunzisoft.keepass.database.element
import android.content.res.Resources
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ObjectNameResource
// Note: We can get away with using int's to store unsigned 32-bit ints // Note: We can get away with using int's to store unsigned 32-bit ints
// since we won't do arithmetic on these values (also unlikely to // since we won't do arithmetic on these values (also unlikely to
// reach negative ids). // reach negative ids).
enum class PwCompressionAlgorithm constructor(val id: Int) { enum class PwCompressionAlgorithm : ObjectNameResource {
None(0), None,
Gzip(1); GZip;
companion object { override fun getName(resources: Resources): String {
return when (this) {
fun fromId(num: Int): PwCompressionAlgorithm? { None -> resources.getString(R.string.compression_none)
for (e in values()) { GZip -> resources.getString(R.string.compression_gzip)
if (e.id == num) {
return e
}
}
return null
} }
} }

View File

@@ -19,9 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.util.Log import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
import com.kunzisoft.keepass.database.exception.KeyFileEmptyException import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
import com.kunzisoft.keepass.database.exception.LoadDatabaseKeyFileEmptyException
import com.kunzisoft.keepass.utils.MemoryUtil import com.kunzisoft.keepass.utils.MemoryUtil
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@@ -34,11 +35,19 @@ import java.security.NoSuchAlgorithmException
import java.util.LinkedHashMap import java.util.LinkedHashMap
import java.util.UUID import java.util.UUID
abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Group, Entry>> { abstract class PwDatabase<
GroupId,
Group : PwGroup<GroupId, Group, Entry>,
Entry : PwEntry<Group, Entry>
> {
// Algorithm used to encrypt the database // Algorithm used to encrypt the database
protected var algorithm: PwEncryptionAlgorithm? = null protected var algorithm: PwEncryptionAlgorithm? = null
abstract val kdfEngine: KdfEngine?
abstract val kdfAvailableList: List<KdfEngine>
var masterKey = ByteArray(32) var masterKey = ByteArray(32)
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
@@ -46,8 +55,10 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
var iconFactory = PwIconFactory() var iconFactory = PwIconFactory()
protected set protected set
private var groupIndexes = LinkedHashMap<PwNodeId<*>, Group>() var changeDuplicateId = false
private var entryIndexes = LinkedHashMap<PwNodeId<*>, Entry>()
private var groupIndexes = LinkedHashMap<PwNodeId<GroupId>, Group>()
private var entryIndexes = LinkedHashMap<PwNodeId<UUID>, Entry>()
abstract val version: String abstract val version: String
@@ -67,15 +78,15 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
var rootGroup: Group? = null var rootGroup: Group? = null
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) { fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
masterKey = getMasterKey(key, keyInputStream) masterKey = getMasterKey(key, keyInputStream)
} }
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray { protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyInputStream) val fileKey = getFileKey(keyInputStream)
val passwordKey = getPasswordKey(key) val passwordKey = getPasswordKey(key)
@@ -115,7 +126,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
return messageDigest.digest() return messageDigest.digest()
} }
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray { protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyByteArrayOutputStream = ByteArrayOutputStream() val keyByteArrayOutputStream = ByteArrayOutputStream()
@@ -129,7 +140,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
} }
when (keyData.size.toLong()) { when (keyData.size.toLong()) {
0L -> throw KeyFileEmptyException() 0L -> throw LoadDatabaseKeyFileEmptyException()
32L -> return keyData 32L -> return keyData
64L -> try { 64L -> try {
return hexStringToByteArray(String(keyData)) return hexStringToByteArray(String(keyData))
@@ -156,15 +167,18 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
open fun validatePasswordEncoding(key: String?): Boolean { open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (key == null) if (password == null && !containsKeyFile)
return false return false
if (password == null)
return true
val encoding = passwordEncoding val encoding = passwordEncoding
val bKey: ByteArray val bKey: ByteArray
try { try {
bKey = key.toByteArray(charset(encoding)) bKey = password.toByteArray(charset(encoding))
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
return false return false
} }
@@ -175,7 +189,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
return false return false
} }
return key == reEncoded return password == reEncoded
} }
/* /*
@@ -184,9 +198,9 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
* ------------------------------------- * -------------------------------------
*/ */
abstract fun newGroupId(): PwNodeId<*> abstract fun newGroupId(): PwNodeId<GroupId>
abstract fun newEntryId(): PwNodeId<*> abstract fun newEntryId(): PwNodeId<UUID>
abstract fun createGroup(): Group abstract fun createGroup(): Group
@@ -211,7 +225,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
* ID number to check for * ID number to check for
* @return True if the ID is used, false otherwise * @return True if the ID is used, false otherwise
*/ */
fun isGroupIdUsed(id: PwNodeId<*>): Boolean { fun isGroupIdUsed(id: PwNodeId<GroupId>): Boolean {
return groupIndexes.containsKey(id) return groupIndexes.containsKey(id)
} }
@@ -226,14 +240,21 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
} }
} }
fun getGroupById(id: PwNodeId<*>): Group? { fun getGroupById(id: PwNodeId<GroupId>): Group? {
return this.groupIndexes[id] return this.groupIndexes[id]
} }
fun addGroupIndex(group: Group) { fun addGroupIndex(group: Group) {
val groupId = group.nodeId val groupId = group.nodeId
if (groupIndexes.containsKey(groupId)) { if (groupIndexes.containsKey(groupId)) {
Log.e(TAG, "Error, a group with the same UUID $groupId already exists") if (changeDuplicateId) {
val newGroupId = newGroupId()
group.nodeId = newGroupId
group.parent?.addChildGroup(group)
this.groupIndexes[newGroupId] = group
} else {
throw LoadDatabaseDuplicateUuidException(Type.GROUP, groupId)
}
} else { } else {
this.groupIndexes[groupId] = group this.groupIndexes[groupId] = group
} }
@@ -253,7 +274,7 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
} }
} }
fun isEntryIdUsed(id: PwNodeId<*>): Boolean { fun isEntryIdUsed(id: PwNodeId<UUID>): Boolean {
return entryIndexes.containsKey(id) return entryIndexes.containsKey(id)
} }
@@ -261,15 +282,21 @@ abstract class PwDatabase<Group : PwGroup<*, Group, Entry>, Entry : PwEntry<Grou
return entryIndexes.values return entryIndexes.values
} }
fun getEntryById(id: PwNodeId<*>): Entry? { fun getEntryById(id: PwNodeId<UUID>): Entry? {
return this.entryIndexes[id] return this.entryIndexes[id]
} }
fun addEntryIndex(entry: Entry) { fun addEntryIndex(entry: Entry) {
val entryId = entry.nodeId val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) { if (entryIndexes.containsKey(entryId)) {
// TODO History if (changeDuplicateId) {
Log.e(TAG, "Error, a group with the same UUID $entryId already exists, change the UUID") val newEntryId = newEntryId()
entry.nodeId = newEntryId
entry.parent?.addChildEntry(entry)
this.entryIndexes[newEntryId] = entry
} else {
throw LoadDatabaseDuplicateUuidException(Type.ENTRY, entryId)
}
} else { } else {
this.entryIndexes[entryId] = entry this.entryIndexes[entryId] = entry
} }

View File

@@ -20,7 +20,9 @@
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
import com.kunzisoft.keepass.stream.NullOutputStream import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@@ -28,18 +30,25 @@ import java.security.DigestOutputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
/** class PwDatabaseV3 : PwDatabase<Int, PwGroupV3, PwEntryV3>() {
* @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 private var numKeyEncRounds: Int = 0
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
override val version: String override val version: String
get() = "KeePass 1" get() = "KeePass 1"
init {
kdfListV3.add(KdfFactory.aesKdf)
}
override val kdfEngine: KdfEngine?
get() = kdfListV3[0]
override val kdfAvailableList: List<KdfEngine>
get() = kdfListV3
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm> override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
get() { get() {
val list = ArrayList<PwEncryptionAlgorithm>() val list = ArrayList<PwEncryptionAlgorithm>()
@@ -103,7 +112,7 @@ class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
return newId return newId
} }
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
return if (key != null && keyInputStream != null) { return if (key != null && keyInputStream != null) {

View File

@@ -26,12 +26,9 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CryptoUtil import com.kunzisoft.keepass.crypto.CryptoUtil
import com.kunzisoft.keepass.crypto.engine.AesEngine import com.kunzisoft.keepass.crypto.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.*
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
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.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm
import com.kunzisoft.keepass.utils.VariantDictionary import com.kunzisoft.keepass.utils.VariantDictionary
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.Text import org.w3c.dom.Text
@@ -43,14 +40,15 @@ import java.util.*
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> { class PwDatabaseV4 : PwDatabase<UUID, PwGroupV4, PwEntryV4> {
var hmacKey: ByteArray? = null var hmacKey: ByteArray? = null
private set private set
var dataCipher = AesEngine.CIPHER_UUID var dataCipher = AesEngine.CIPHER_UUID
private var dataEngine: CipherEngine = AesEngine() private var dataEngine: CipherEngine = AesEngine()
var compressionAlgorithm = PwCompressionAlgorithm.Gzip var compressionAlgorithm = PwCompressionAlgorithm.GZip
var kdfParameters: KdfParameters? = null var kdfParameters: KdfParameters? = null
private var kdfV4List: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0 private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary() var publicCustomData = VariantDictionary()
@@ -93,6 +91,11 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
var localizedAppName = "KeePassDX" // TODO resource var localizedAppName = "KeePassDX" // TODO resource
init {
kdfV4List.add(KdfFactory.aesKdf)
kdfV4List.add(KdfFactory.argon2Kdf)
}
constructor() constructor()
constructor(databaseName: String) { constructor(databaseName: String) {
@@ -107,6 +110,39 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
override val version: String override val version: String
get() = "KeePass 2" get() = "KeePass 2"
override val kdfEngine: KdfEngine?
get() = try {
getEngineV4(kdfParameters)
} catch (unknownKDF: UnknownKDF) {
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
null
}
override val kdfAvailableList: List<KdfEngine>
get() = kdfV4List
@Throws(UnknownKDF::class)
fun getEngineV4(kdfParameters: KdfParameters?): KdfEngine {
val unknownKDFException = UnknownKDF()
if (kdfParameters == null) {
throw unknownKDFException
}
for (engine in kdfV4List) {
if (engine.uuid == kdfParameters.uuid) {
return engine
}
}
throw unknownKDFException
}
val availableCompressionAlgorithms: List<PwCompressionAlgorithm>
get() {
val list = ArrayList<PwCompressionAlgorithm>()
list.add(PwCompressionAlgorithm.None)
list.add(PwCompressionAlgorithm.GZip)
return list
}
override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm> override val availableEncryptionAlgorithms: List<PwEncryptionAlgorithm>
get() { get() {
val list = ArrayList<PwEncryptionAlgorithm>() val list = ArrayList<PwEncryptionAlgorithm>()
@@ -116,45 +152,45 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
return list return list
} }
val kdfEngine: KdfEngine?
get() {
return try {
KdfFactory.getEngineV4(kdfParameters)
} catch (unknownKDF: UnknownKDF) {
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
null
}
}
override var numberKeyEncryptionRounds: Long override var numberKeyEncryptionRounds: Long
get() { get() {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null) if (kdfEngine != null && kdfParameters != null)
numKeyEncRounds = kdfEngine!!.getKeyRounds(kdfParameters!!) numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
return numKeyEncRounds return numKeyEncRounds
} }
@Throws(NumberFormatException::class) @Throws(NumberFormatException::class)
set(rounds) { set(rounds) {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null) if (kdfEngine != null && kdfParameters != null)
kdfEngine!!.setKeyRounds(kdfParameters!!, rounds) kdfEngine.setKeyRounds(kdfParameters!!, rounds)
numKeyEncRounds = rounds numKeyEncRounds = rounds
} }
var memoryUsage: Long var memoryUsage: Long
get() = if (kdfEngine != null && kdfParameters != null) { get() {
kdfEngine!!.getMemoryUsage(kdfParameters!!) val kdfEngine = kdfEngine
} else KdfEngine.UNKNOWN_VALUE.toLong() return if (kdfEngine != null && kdfParameters != null) {
kdfEngine.getMemoryUsage(kdfParameters!!)
} else KdfEngine.UNKNOWN_VALUE.toLong()
}
set(memory) { set(memory) {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null) if (kdfEngine != null && kdfParameters != null)
kdfEngine!!.setMemoryUsage(kdfParameters!!, memory) kdfEngine.setMemoryUsage(kdfParameters!!, memory)
} }
var parallelism: Int var parallelism: Int
get() = if (kdfEngine != null && kdfParameters != null) { get() {
kdfEngine!!.getParallelism(kdfParameters!!) val kdfEngine = kdfEngine
} else KdfEngine.UNKNOWN_VALUE return if (kdfEngine != null && kdfParameters != null) {
kdfEngine.getParallelism(kdfParameters!!)
} else KdfEngine.UNKNOWN_VALUE
}
set(parallelism) { set(parallelism) {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null) if (kdfEngine != null && kdfParameters != null)
kdfEngine!!.setParallelism(kdfParameters!!, parallelism) kdfEngine.setParallelism(kdfParameters!!, parallelism)
} }
override val passwordEncoding: String override val passwordEncoding: String
@@ -200,7 +236,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
return getCustomData().isNotEmpty() return getCustomData().isNotEmpty()
} }
@Throws(InvalidKeyFileException::class, IOException::class) @Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray { public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
var masterKey = byteArrayOf() var masterKey = byteArrayOf()
@@ -227,7 +263,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
fun makeFinalKey(masterSeed: ByteArray) { fun makeFinalKey(masterSeed: ByteArray) {
kdfParameters?.let { keyDerivationFunctionParameters -> kdfParameters?.let { keyDerivationFunctionParameters ->
val kdfEngine = KdfFactory.getEngineV4(keyDerivationFunctionParameters) val kdfEngine = getEngineV4(keyDerivationFunctionParameters)
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters) var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
if (transformedMasterKey.size != 32) { if (transformedMasterKey.size != 32) {
@@ -360,9 +396,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
} }
addGroupTo(recycleBinGroup, rootGroup) addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id recycleBinUUID = recycleBinGroup.id
recycleBinGroup.lastModificationTime.date?.let { recycleBinChanged = recycleBinGroup.lastModificationTime.date
recycleBinChanged = it
}
} }
} }
@@ -427,10 +461,10 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
return publicCustomData.size() > 0 return publicCustomData.size() > 0
} }
override fun validatePasswordEncoding(key: String?): Boolean { override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (key == null) if (password == null)
return true return true
return super.validatePasswordEncoding(key) return super.validatePasswordEncoding(password, containsKeyFile)
} }
override fun clearCache() { override fun clearCache() {

View File

@@ -19,14 +19,12 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.content.res.Resources
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import com.kunzisoft.keepass.utils.Types import com.kunzisoft.keepass.utils.Types
import java.util.*
import java.util.Arrays
import java.util.Calendar
import java.util.Date
/** /**
* Converting from the C Date format to the Java data format is * Converting from the C Date format to the Java data format is
@@ -34,14 +32,14 @@ import java.util.Date
*/ */
class PwDate : Parcelable { class PwDate : Parcelable {
private var jDate: Date? = null private var jDate: Date = Date()
private var jDateBuilt = false private var jDateBuilt = false
@Transient @Transient
private var cDate: ByteArray? = null private var cDate: ByteArray? = null
@Transient @Transient
private var cDateBuilt = false private var cDateBuilt = false
val date: Date? val date: Date
get() { get() {
if (!jDateBuilt) { if (!jDateBuilt) {
jDate = readTime(cDate, 0, calendar) jDate = readTime(cDate, 0, calendar)
@@ -68,9 +66,7 @@ class PwDate : Parcelable {
} }
constructor(source: PwDate) { constructor(source: PwDate) {
if (source.jDate != null) { this.jDate = Date(source.jDate.time)
this.jDate = Date(source.jDate!!.time)
}
this.jDateBuilt = source.jDateBuilt this.jDateBuilt = source.jDateBuilt
if (source.cDate != null) { if (source.cDate != null) {
@@ -106,6 +102,10 @@ class PwDate : Parcelable {
return 0 return 0
} }
fun getDateTimeString(resources: Resources): String {
return Companion.getDateTimeString(resources, this.date)
}
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(date) dest.writeSerializable(date)
dest.writeByte((if (jDateBuilt) 1 else 0).toByte()) dest.writeByte((if (jDateBuilt) 1 else 0).toByte())
@@ -135,7 +135,7 @@ class PwDate : Parcelable {
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = jDate?.hashCode() ?: 0 var result = jDate.hashCode()
result = 31 * result + jDateBuilt.hashCode() result = 31 * result + jDateBuilt.hashCode()
result = 31 * result + (cDate?.contentHashCode() ?: 0) result = 31 * result + (cDate?.contentHashCode() ?: 0)
result = 31 * result + cDateBuilt.hashCode() result = 31 * result + cDateBuilt.hashCode()
@@ -280,5 +280,13 @@ class PwDate : Parcelable {
cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND) cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND)
} }
fun getDateTimeString(resources: Resources, date: Date): String {
return java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.MEDIUM,
java.text.DateFormat.MEDIUM,
ConfigurationCompat.getLocales(resources.configuration)[0])
.format(date)
}
} }
} }

View File

@@ -129,7 +129,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
* Update with deep copy of each entry element * Update with deep copy of each entry element
* @param source * @param source
*/ */
fun updateWith(source: PwEntryV4) { fun updateWith(source: PwEntryV4, copyHistory: Boolean = true) {
super.updateWith(source) super.updateWith(source)
iconCustom = PwIconCustom(source.iconCustom) iconCustom = PwIconCustom(source.iconCustom)
usageCount = source.usageCount usageCount = source.usageCount
@@ -146,7 +146,8 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
overrideURL = source.overrideURL overrideURL = source.overrideURL
autoType = AutoType(source.autoType) autoType = AutoType(source.autoType)
history.clear() history.clear()
history.addAll(source.history) if (copyHistory)
history.addAll(source.history)
url = source.url url = source.url
additional = source.additional additional = source.additional
tags = source.tags tags = source.tags
@@ -263,6 +264,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
return true return true
} }
fun removeAllFields() {
fields.clear()
}
fun addExtraField(label: String, value: ProtectedString) { fun addExtraField(label: String, value: ProtectedString) {
fields[label] = value fields[label] = value
} }
@@ -287,6 +292,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
history.add(entry) history.add(entry)
} }
fun removeAllHistory() {
history.clear()
}
fun removeOldestEntryFromHistory() { fun removeOldestEntryFromHistory() {
var min: Date? = null var min: Date? = null
var index = -1 var index = -1
@@ -294,7 +303,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
for (i in history.indices) { for (i in history.indices) {
val entry = history[i] val entry = history[i]
val lastMod = entry.lastModificationTime.date val lastMod = entry.lastModificationTime.date
if (min == null || lastMod == null || lastMod.before(min)) { if (min == null || lastMod.before(min)) {
index = i index = i
min = lastMod min = lastMod
} }

View File

@@ -30,7 +30,9 @@ abstract class PwGroup
protected fun updateWith(source: PwGroup<Id, Group, Entry>) { protected fun updateWith(source: PwGroup<Id, Group, Entry>) {
super.updateWith(source) super.updateWith(source)
titleGroup = source.titleGroup titleGroup = source.titleGroup
childGroups.clear()
childGroups.addAll(source.childGroups) childGroups.addAll(source.childGroups)
childEntries.clear()
childEntries.addAll(source.childEntries) childEntries.addAll(source.childEntries)
} }

View File

@@ -90,7 +90,7 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
final override var isExpires: Boolean final override var isExpires: Boolean
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure) // If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
get() = expiryTime.date get() = expiryTime.date
?.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate()) ?: true .before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate())
set(value) { set(value) {
if (!value) { if (!value) {
expiryTime = PwDate.PW_NEVER_EXPIRE expiryTime = PwDate.PW_NEVER_EXPIRE

View File

@@ -1,27 +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.exception
class ArcFourException : InvalidDBException() {
companion object {
private const val serialVersionUID = 2103983626687861237L
}
}

View File

@@ -1,40 +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.exception
import android.net.Uri
import java.io.FileNotFoundException
class ContentFileNotFoundException : FileNotFoundException() {
companion object {
fun getInstance(uri: Uri?): FileNotFoundException {
if (uri == null) {
return FileNotFoundException()
}
val scheme = uri.scheme
return if (scheme != null
&& scheme.isNotEmpty()
&& scheme.equals("content", ignoreCase = true)) {
ContentFileNotFoundException()
} else FileNotFoundException()
}
}
}

View File

@@ -19,14 +19,10 @@
*/ */
package com.kunzisoft.keepass.database.exception package com.kunzisoft.keepass.database.exception
class PwDbOutputException : Exception { class DatabaseOutputException : Exception {
constructor(string: String) : super(string) constructor(string: String) : super(string)
constructor(string: String, e: Exception) : super(string, e) constructor(string: String, e: Exception) : super(string, e)
constructor(e: Exception) : super(e) constructor(e: Exception) : super(e)
companion object {
private const val serialVersionUID = 3321212743159473368L
}
} }

View File

@@ -1,27 +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.exception
class InvalidAlgorithmException : InvalidDBException() {
companion object {
private const val serialVersionUID = 3062682891863487208L
}
}

View File

@@ -1,32 +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.exception
open class InvalidDBException : Exception {
constructor(str: String) : super(str)
constructor() : super()
companion object {
private const val serialVersionUID = 5191964825154190923L
}
}

View File

@@ -1,27 +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.exception
class InvalidDBSignatureException : InvalidDBException() {
companion object {
private const val serialVersionUID = -5358923878743513758L
}
}

View File

@@ -1,27 +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.exception
class InvalidDBVersionException : InvalidDBException() {
companion object {
private const val serialVersionUID = -4260650987856400586L
}
}

View File

@@ -1,25 +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.exception
open class InvalidKeyFileException : InvalidDBException() {
companion object {
private const val serialVersionUID = 5540694419562294464L
}
}

View File

@@ -1,26 +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.exception
class InvalidPasswordException : InvalidDBException() {
companion object {
private const val serialVersionUID = -8729476180242058319L
}
}

View File

@@ -1,26 +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.exception
class KeyFileEmptyException : InvalidKeyFileException() {
companion object {
private const val serialVersionUID = -1630780661204212325L
}
}

View File

@@ -0,0 +1,75 @@
package com.kunzisoft.keepass.database.exception
import android.content.res.Resources
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.PwNodeId
import com.kunzisoft.keepass.database.element.Type
import java.io.IOException
class LoadDatabaseArcFourException :
LoadDatabaseException(R.string.error_arc4)
class LoadDatabaseFileNotFoundException :
LoadDatabaseException(R.string.file_not_found_content)
class LoadDatabaseInvalidAlgorithmException :
LoadDatabaseException(R.string.invalid_algorithm)
class LoadDatabaseDuplicateUuidException(type: Type, uuid: PwNodeId<*>):
LoadDatabaseException(R.string.invalid_db_same_uuid, type.name, uuid.toString())
class LoadDatabaseIOException(exception: IOException) :
LoadDatabaseException(exception, R.string.error_load_database)
class LoadDatabaseKDFMemoryException(exception: IOException) :
LoadDatabaseException(exception, R.string.error_load_database_KDF_memory)
class LoadDatabaseSignatureException :
LoadDatabaseException(R.string.invalid_db_sig)
class LoadDatabaseVersionException :
LoadDatabaseException(R.string.unsupported_db_version)
open class LoadDatabaseInvalidKeyFileException :
LoadDatabaseException(R.string.keyfile_does_not_exist)
class LoadDatabaseInvalidPasswordException :
LoadDatabaseException(R.string.invalid_password)
class LoadDatabaseKeyFileEmptyException :
LoadDatabaseException(R.string.keyfile_is_empty)
class LoadDatabaseNoMemoryException(exception: OutOfMemoryError) :
LoadDatabaseException(exception, R.string.error_out_of_memory)
open class LoadDatabaseException : Exception {
@StringRes
var errorId: Int = R.string.error_load_database
var parameters: (Array<out String>)? = null
constructor(errorMessageId: Int) : super() {
errorId = errorMessageId
}
constructor(errorMessageId: Int, vararg params: String) : super() {
errorId = errorMessageId
parameters = params
}
constructor(throwable: Throwable, errorMessageId: Int? = null) : super(throwable) {
errorMessageId?.let {
errorId = it
}
}
constructor() : super()
fun getLocalizedMessage(resources: Resources): String {
parameters?.let {
return resources.getString(errorId, *it)
} ?: return resources.getString(errorId)
}
}

View File

@@ -19,9 +19,4 @@
*/ */
package com.kunzisoft.keepass.database.exception package com.kunzisoft.keepass.database.exception
class SamsungClipboardException(e: Exception) : Exception(e) { class SamsungClipboardException(e: Exception) : Exception(e)
companion object {
private const val serialVersionUID = -3168837280393843509L
}
}

View File

@@ -2,8 +2,4 @@ package com.kunzisoft.keepass.database.exception
import java.io.IOException import java.io.IOException
class UnknownKDF : IOException(message) { class UnknownKDF : IOException("Unknown key derivation function")
companion object {
private const val message = "Unknown key derivation function"
}
}

View File

@@ -24,11 +24,8 @@ import com.kunzisoft.keepass.crypto.keyDerivation.AesKdf
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.NodeHandler import com.kunzisoft.keepass.database.NodeHandler
import com.kunzisoft.keepass.database.element.PwNodeV4Interface import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.PwDatabaseV4 import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException
import com.kunzisoft.keepass.database.element.PwEntryV4
import com.kunzisoft.keepass.database.element.PwGroupV4
import com.kunzisoft.keepass.database.exception.InvalidDBVersionException
import com.kunzisoft.keepass.stream.CopyInputStream import com.kunzisoft.keepass.stream.CopyInputStream
import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.HmacBlockStream
import com.kunzisoft.keepass.stream.LEDataInputStream import com.kunzisoft.keepass.stream.LEDataInputStream
@@ -51,10 +48,10 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
// version < FILE_VERSION_32_4) // version < FILE_VERSION_32_4)
var transformSeed: ByteArray? var transformSeed: ByteArray?
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.ParamSeed) get() = databaseV4.kdfParameters?.getByteArray(AesKdf.PARAM_SEED)
private set(seed) { private set(seed) {
assignAesKdfEngineIfNotExists() assignAesKdfEngineIfNotExists()
databaseV4.kdfParameters?.setByteArray(AesKdf.ParamSeed, seed) databaseV4.kdfParameters?.setByteArray(AesKdf.PARAM_SEED, seed)
} }
object PwDbHeaderV4Fields { object PwDbHeaderV4Fields {
@@ -133,9 +130,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
/** Assumes the input stream is at the beginning of the .kdbx file /** Assumes the input stream is at the beginning of the .kdbx file
* @param inputStream * @param inputStream
* @throws IOException * @throws IOException
* @throws InvalidDBVersionException * @throws LoadDatabaseVersionException
*/ */
@Throws(IOException::class, InvalidDBVersionException::class) @Throws(IOException::class, LoadDatabaseVersionException::class)
fun loadFromFile(inputStream: InputStream): HeaderAndHash { fun loadFromFile(inputStream: InputStream): HeaderAndHash {
val messageDigest: MessageDigest val messageDigest: MessageDigest
try { try {
@@ -153,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
val sig2 = littleEndianDataInputStream.readInt() val sig2 = littleEndianDataInputStream.readInt()
if (!matchesHeader(sig1, sig2)) { if (!matchesHeader(sig1, sig2)) {
throw InvalidDBVersionException() throw LoadDatabaseVersionException()
} }
version = littleEndianDataInputStream.readUInt() // Erase previous value version = littleEndianDataInputStream.readUInt() // Erase previous value
if (!validVersion(version)) { if (!validVersion(version)) {
throw InvalidDBVersionException() throw LoadDatabaseVersionException()
} }
var done = false var done = false
@@ -229,7 +226,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
} }
private fun assignAesKdfEngineIfNotExists() { private fun assignAesKdfEngineIfNotExists() {
if (databaseV4.kdfParameters == null || databaseV4.kdfParameters!!.uuid != KdfFactory.aesKdf.uuid) { val kdfParams = databaseV4.kdfParameters
if (kdfParams == null
|| kdfParams.uuid != KdfFactory.aesKdf.uuid) {
databaseV4.kdfParameters = KdfFactory.aesKdf.defaultParameters databaseV4.kdfParameters = KdfFactory.aesKdf.defaultParameters
} }
} }
@@ -246,7 +245,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
private fun setTransformRound(roundsByte: ByteArray?) { private fun setTransformRound(roundsByte: ByteArray?) {
assignAesKdfEngineIfNotExists() assignAesKdfEngineIfNotExists()
val rounds = LEDataInputStream.readLong(roundsByte!!, 0) val rounds = LEDataInputStream.readLong(roundsByte!!, 0)
databaseV4.kdfParameters?.setUInt64(AesKdf.ParamRounds, rounds) databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
databaseV4.numberKeyEncryptionRounds = rounds databaseV4.numberKeyEncryptionRounds = rounds
} }
@@ -261,7 +260,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
throw IOException("Unrecognized compression flag.") throw IOException("Unrecognized compression flag.")
} }
PwCompressionAlgorithm.fromId(flag)?.let { compression -> getCompressionFromFlag(flag)?.let { compression ->
databaseV4.compressionAlgorithm = compression databaseV4.compressionAlgorithm = compression
} }
} }
@@ -299,6 +298,21 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
const val FILE_VERSION_32_3: Long = 0x00030001 const val FILE_VERSION_32_3: Long = 0x00030001
const val FILE_VERSION_32_4: Long = 0x00040000 const val FILE_VERSION_32_4: Long = 0x00040000
fun getCompressionFromFlag(flag: Int): PwCompressionAlgorithm? {
return when (flag) {
0 -> PwCompressionAlgorithm.None
1 -> PwCompressionAlgorithm.GZip
else -> null
}
}
fun getFlagFromCompression(compression: PwCompressionAlgorithm): Int {
return when (compression) {
PwCompressionAlgorithm.GZip -> 1
else -> 0
}
}
fun matchesHeader(sig1: Int, sig2: Int): Boolean { fun matchesHeader(sig1: Int, sig2: Int): Boolean {
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2) return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
} }

View File

@@ -20,13 +20,13 @@
package com.kunzisoft.keepass.database.file.load package com.kunzisoft.keepass.database.file.load
import com.kunzisoft.keepass.database.element.PwDatabase import com.kunzisoft.keepass.database.element.PwDatabase
import com.kunzisoft.keepass.database.exception.InvalidDBException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
abstract class Importer<PwDb : PwDatabase<*, *>> { abstract class Importer<PwDb : PwDatabase<*, *, *>> {
/** /**
* Load a versioned database file, return contents in a new PwDatabase. * Load a versioned database file, return contents in a new PwDatabase.
@@ -36,9 +36,9 @@ abstract class Importer<PwDb : PwDatabase<*, *>> {
* @return new PwDatabase container. * @return new PwDatabase container.
* *
* @throws IOException on any file error. * @throws IOException on any file error.
* @throws InvalidDBException on database error. * @throws LoadDatabaseException on database error.
*/ */
@Throws(IOException::class, InvalidDBException::class) @Throws(IOException::class, LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyInputStream: InputStream?,

View File

@@ -73,7 +73,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
private lateinit var mDatabaseToOpen: PwDatabaseV3 private lateinit var mDatabaseToOpen: PwDatabaseV3
@Throws(IOException::class, InvalidDBException::class) @Throws(IOException::class, LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyInputStream: InputStream?,
@@ -92,11 +92,11 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
hdr.loadFromFile(filebuf, 0) hdr.loadFromFile(filebuf, 0)
if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) { if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) {
throw InvalidDBSignatureException() throw LoadDatabaseSignatureException()
} }
if (!hdr.matchesVersion()) { if (!hdr.matchesVersion()) {
throw InvalidDBVersionException() throw LoadDatabaseVersionException()
} }
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
@@ -109,7 +109,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
} else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) { } else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) {
mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish
} else { } else {
throw InvalidAlgorithmException() throw LoadDatabaseInvalidAlgorithmException()
} }
mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong() mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
@@ -152,7 +152,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
} catch (e1: IllegalBlockSizeException) { } catch (e1: IllegalBlockSizeException) {
throw IOException("Invalid block size") throw IOException("Invalid block size")
} catch (e1: BadPaddingException) { } catch (e1: BadPaddingException) {
throw InvalidPasswordException() throw LoadDatabaseInvalidPasswordException()
} }
val md: MessageDigest val md: MessageDigest
@@ -171,7 +171,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
if (!Arrays.equals(hash, hdr.contentsHash)) { if (!Arrays.equals(hash, hdr.contentsHash)) {
Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)") Log.w(TAG, "Database file did not decrypt correctly. (checksum code is broken)")
throw InvalidPasswordException() throw LoadDatabaseInvalidPasswordException()
} }
// New manual root because V3 contains multiple root groups (here available with getRootGroups()) // New manual root because V3 contains multiple root groups (here available with getRootGroups())

View File

@@ -24,11 +24,11 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.exception.ArcFourException import com.kunzisoft.keepass.database.exception.LoadDatabaseArcFourException
import com.kunzisoft.keepass.database.exception.InvalidDBException import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidPasswordException import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidPasswordException
import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.element.security.ProtectedBinary import com.kunzisoft.keepass.database.element.security.ProtectedBinary
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -56,7 +56,8 @@ import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException import javax.crypto.NoSuchPaddingException
import kotlin.math.min import kotlin.math.min
class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() { class ImporterV4(private val streamDir: File,
private val fixDuplicateUUID: Boolean = false) : Importer<PwDatabaseV4>() {
private var randomStream: StreamCipher? = null private var randomStream: StreamCipher? = null
private lateinit var mDatabase: PwDatabaseV4 private lateinit var mDatabase: PwDatabaseV4
@@ -89,7 +90,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
private var entryCustomDataKey: String? = null private var entryCustomDataKey: String? = null
private var entryCustomDataValue: String? = null private var entryCustomDataValue: String? = null
@Throws(IOException::class, InvalidDBException::class) @Throws(IOException::class, LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
password: String?, password: String?,
keyInputStream: InputStream?, keyInputStream: InputStream?,
@@ -99,6 +100,9 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabase = PwDatabaseV4() mDatabase = PwDatabaseV4()
mDatabase.changeDuplicateId = fixDuplicateUUID
val header = PwDbHeaderV4(mDatabase) val header = PwDbHeaderV4(mDatabase)
val headerAndHash = header.loadFromFile(databaseInputStream) val headerAndHash = header.loadFromFile(databaseInputStream)
@@ -138,14 +142,14 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
try { try {
storedStartBytes = dataDecrypted.readBytes(32) storedStartBytes = dataDecrypted.readBytes(32)
if (storedStartBytes == null || storedStartBytes.size != 32) { if (storedStartBytes == null || storedStartBytes.size != 32) {
throw InvalidPasswordException() throw LoadDatabaseInvalidPasswordException()
} }
} catch (e: IOException) { } catch (e: IOException) {
throw InvalidPasswordException() throw LoadDatabaseInvalidPasswordException()
} }
if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) { if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
throw InvalidPasswordException() throw LoadDatabaseInvalidPasswordException()
} }
isPlain = HashedBlockInputStream(dataDecrypted) isPlain = HashedBlockInputStream(dataDecrypted)
@@ -153,18 +157,18 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
val isData = LEDataInputStream(databaseInputStream) val isData = LEDataInputStream(databaseInputStream)
val storedHash = isData.readBytes(32) val storedHash = isData.readBytes(32)
if (!Arrays.equals(storedHash, hashOfHeader)) { if (!Arrays.equals(storedHash, hashOfHeader)) {
throw InvalidDBException() throw LoadDatabaseException()
} }
val hmacKey = mDatabase.hmacKey ?: throw InvalidDBException() val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey) val headerHmac = PwDbHeaderV4.computeHeaderHmac(pbHeader, hmacKey)
val storedHmac = isData.readBytes(32) val storedHmac = isData.readBytes(32)
if (storedHmac == null || storedHmac.size != 32) { if (storedHmac == null || storedHmac.size != 32) {
throw InvalidDBException() throw LoadDatabaseException()
} }
// Mac doesn't match // Mac doesn't match
if (!Arrays.equals(headerHmac, storedHmac)) { if (!Arrays.equals(headerHmac, storedHmac)) {
throw InvalidDBException() throw LoadDatabaseException()
} }
val hmIs = HmacBlockInputStream(isData, true, hmacKey) val hmIs = HmacBlockInputStream(isData, true, hmacKey)
@@ -173,10 +177,9 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
} }
val isXml: InputStream val isXml: InputStream
if (mDatabase.compressionAlgorithm === PwCompressionAlgorithm.Gzip) { isXml = when(mDatabase.compressionAlgorithm) {
isXml = GZIPInputStream(isPlain) PwCompressionAlgorithm.GZip -> GZIPInputStream(isPlain)
} else { else -> isPlain
isXml = isPlain
} }
if (version >= PwDbHeaderV4.FILE_VERSION_32_4) { if (version >= PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -186,7 +189,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
if (randomStream == null) { if (randomStream == null) {
throw ArcFourException() throw LoadDatabaseArcFourException()
} }
readXmlStreamed(isXml) readXmlStreamed(isXml)
@@ -271,7 +274,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
Binaries Binaries
} }
@Throws(IOException::class, InvalidDBException::class) @Throws(IOException::class, LoadDatabaseException::class)
private fun readXmlStreamed(readerStream: InputStream) { private fun readXmlStreamed(readerStream: InputStream) {
try { try {
readDocumentStreamed(createPullParser(readerStream)) readDocumentStreamed(createPullParser(readerStream))
@@ -282,7 +285,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
} }
@Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class) @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
private fun readDocumentStreamed(xpp: XmlPullParser) { private fun readDocumentStreamed(xpp: XmlPullParser) {
ctxGroups.clear() ctxGroups.clear()
@@ -313,7 +316,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
if (ctxGroups.size != 0) throw IOException("Malformed") if (ctxGroups.size != 0) throw IOException("Malformed")
} }
@Throws(XmlPullParserException::class, IOException::class, InvalidDBException::class) @Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext { private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
val name = xpp.name val name = xpp.name
when (ctx) { when (ctx) {
@@ -337,7 +340,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
if (encodedHash.isNotEmpty() && hashOfHeader != null) { if (encodedHash.isNotEmpty() && hashOfHeader != null) {
val hash = Base64Coder.decode(encodedHash) val hash = Base64Coder.decode(encodedHash)
if (!Arrays.equals(hash, hashOfHeader)) { if (!Arrays.equals(hash, hashOfHeader)) {
throw InvalidDBException() throw LoadDatabaseException()
} }
} }
} else if (name.equals(PwDatabaseV4XML.ElemSettingsChanged, ignoreCase = true)) { } else if (name.equals(PwDatabaseV4XML.ElemSettingsChanged, ignoreCase = true)) {
@@ -355,7 +358,6 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
} else if (name.equals(PwDatabaseV4XML.ElemDbDefaultUserChanged, ignoreCase = true)) { } else if (name.equals(PwDatabaseV4XML.ElemDbDefaultUserChanged, ignoreCase = true)) {
mDatabase.defaultUserNameChanged = readPwTime(xpp) mDatabase.defaultUserNameChanged = readPwTime(xpp)
} else if (name.equals(PwDatabaseV4XML.ElemDbColor, ignoreCase = true)) { } else if (name.equals(PwDatabaseV4XML.ElemDbColor, ignoreCase = true)) {
// TODO: Add support to interpret the color if we want to allow changing the database color
mDatabase.color = readString(xpp) mDatabase.color = readString(xpp)
} else if (name.equals(PwDatabaseV4XML.ElemDbMntncHistoryDays, ignoreCase = true)) { } else if (name.equals(PwDatabaseV4XML.ElemDbMntncHistoryDays, ignoreCase = true)) {
mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS) mDatabase.maintenanceHistoryDays = readUInt(xpp, DEFAULT_HISTORY_DAYS)

View File

@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.element.PwDatabaseV4 import com.kunzisoft.keepass.database.element.PwDatabaseV4
import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeader
import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.exception.PwDbOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.stream.HmacBlockStream import com.kunzisoft.keepass.stream.HmacBlockStream
import com.kunzisoft.keepass.stream.LEDataOutputStream import com.kunzisoft.keepass.stream.LEDataOutputStream
import com.kunzisoft.keepass.stream.MacOutputStream import com.kunzisoft.keepass.stream.MacOutputStream
@@ -41,7 +41,7 @@ import java.security.NoSuchAlgorithmException
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
class PwDbHeaderOutputV4 @Throws(PwDbOutputException::class) class PwDbHeaderOutputV4 @Throws(DatabaseOutputException::class)
constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: OutputStream) : PwDbHeaderOutput() { constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os: OutputStream) : PwDbHeaderOutput() {
private val los: LEDataOutputStream private val los: LEDataOutputStream
private val mos: MacOutputStream private val mos: MacOutputStream
@@ -54,13 +54,13 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
try { try {
md = MessageDigest.getInstance("SHA-256") md = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("SHA-256 not implemented here.") throw DatabaseOutputException("SHA-256 not implemented here.")
} }
try { try {
db.makeFinalKey(header.masterSeed) db.makeFinalKey(header.masterSeed)
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} }
val hmac: Mac val hmac: Mac
@@ -69,9 +69,9 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, Types.ULONG_MAX_VALUE), "HmacSHA256") val signingKey = SecretKeySpec(HmacBlockStream.GetHmacKey64(db.hmacKey, Types.ULONG_MAX_VALUE), "HmacSHA256")
hmac.init(signingKey) hmac.init(signingKey)
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} catch (e: InvalidKeyException) { } catch (e: InvalidKeyException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} }
dos = DigestOutputStream(os, md) dos = DigestOutputStream(os, md)
@@ -86,9 +86,8 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
los.writeUInt(PwDbHeaderV4.DBSIG_2.toLong()) los.writeUInt(PwDbHeaderV4.DBSIG_2.toLong())
los.writeUInt(header.version) los.writeUInt(header.version)
writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CipherID, Types.UUIDtoBytes(db.dataCipher)) writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CipherID, Types.UUIDtoBytes(db.dataCipher))
writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(db.compressionAlgorithm.id)) writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.CompressionFlags, LEDataOutputStream.writeIntBuf(PwDbHeaderV4.getFlagFromCompression(db.compressionAlgorithm)))
writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.MasterSeed, header.masterSeed) writeHeaderField(PwDbHeaderV4.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) { if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.database.file.save package com.kunzisoft.keepass.database.file.save
import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeader
import com.kunzisoft.keepass.database.exception.PwDbOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import java.io.OutputStream import java.io.OutputStream
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
@@ -28,13 +28,13 @@ import java.security.SecureRandom
abstract class PwDbOutput<Header : PwDbHeader> protected constructor(protected var mOS: OutputStream) { abstract class PwDbOutput<Header : PwDbHeader> protected constructor(protected var mOS: OutputStream) {
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom { protected open fun setIVs(header: Header): SecureRandom {
val random: SecureRandom val random: SecureRandom
try { try {
random = SecureRandom.getInstance("SHA1PRNG") random = SecureRandom.getInstance("SHA1PRNG")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("Does not support secure random number generation.") throw DatabaseOutputException("Does not support secure random number generation.")
} }
random.nextBytes(header.encryptionIV) random.nextBytes(header.encryptionIV)
@@ -43,10 +43,10 @@ abstract class PwDbOutput<Header : PwDbHeader> protected constructor(protected v
return random return random
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
abstract fun output() abstract fun output()
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
abstract fun outputHeader(outputStream: OutputStream): Header abstract fun outputHeader(outputStream: OutputStream): Header
} }

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.database.file.save
import com.kunzisoft.keepass.crypto.CipherFactory import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.exception.PwDbOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.PwDbHeader import com.kunzisoft.keepass.database.file.PwDbHeader
import com.kunzisoft.keepass.database.file.PwDbHeaderV3 import com.kunzisoft.keepass.database.file.PwDbHeaderV3
import com.kunzisoft.keepass.stream.LEDataOutputStream import com.kunzisoft.keepass.stream.LEDataOutputStream
@@ -42,18 +42,18 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
private var headerHashBlock: ByteArray? = null private var headerHashBlock: ByteArray? = null
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
fun getFinalKey(header: PwDbHeader): ByteArray? { fun getFinalKey(header: PwDbHeader): ByteArray? {
try { try {
val h3 = header as PwDbHeaderV3 val h3 = header as PwDbHeaderV3
mDatabaseV3.makeFinalKey(h3.masterSeed, h3.transformSeed, mDatabaseV3.numberKeyEncryptionRounds) mDatabaseV3.makeFinalKey(h3.masterSeed, h3.transformSeed, mDatabaseV3.numberKeyEncryptionRounds)
return mDatabaseV3.finalKey return mDatabaseV3.finalKey
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Key creation failed.", e) throw DatabaseOutputException("Key creation failed.", e)
} }
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
override fun output() { override fun output() {
// Before we output the header, we should sort our list of groups // Before we output the header, we should sort our list of groups
// and remove any orphaned nodes that are no longer part of the tree hierarchy // and remove any orphaned nodes that are no longer part of the tree hierarchy
@@ -74,7 +74,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
throw Exception() throw Exception()
} }
} catch (e: Exception) { } catch (e: Exception) {
throw PwDbOutputException("Algorithm not supported.", e) throw DatabaseOutputException("Algorithm not supported.", e)
} }
try { try {
@@ -86,23 +86,23 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
bos.close() bos.close()
} catch (e: InvalidKeyException) { } catch (e: InvalidKeyException) {
throw PwDbOutputException("Invalid key", e) throw DatabaseOutputException("Invalid key", e)
} catch (e: InvalidAlgorithmParameterException) { } catch (e: InvalidAlgorithmParameterException) {
throw PwDbOutputException("Invalid algorithm parameter.", e) throw DatabaseOutputException("Invalid algorithm parameter.", e)
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Failed to output final encrypted part.", e) throw DatabaseOutputException("Failed to output final encrypted part.", e)
} }
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
override fun setIVs(header: PwDbHeaderV3): SecureRandom { override fun setIVs(header: PwDbHeaderV3): SecureRandom {
val random = super.setIVs(header) val random = super.setIVs(header)
random.nextBytes(header.transformSeed) random.nextBytes(header.transformSeed)
return random return random
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): PwDbHeaderV3 { override fun outputHeader(outputStream: OutputStream): PwDbHeaderV3 {
// Build header // Build header
val header = PwDbHeaderV3() val header = PwDbHeaderV3()
@@ -115,7 +115,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
} else if (mDatabaseV3.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) { } else if (mDatabaseV3.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) {
header.flags = header.flags or PwDbHeaderV3.FLAG_TWOFISH header.flags = header.flags or PwDbHeaderV3.FLAG_TWOFISH
} else { } else {
throw PwDbOutputException("Unsupported algorithm.") throw DatabaseOutputException("Unsupported algorithm.")
} }
header.version = PwDbHeaderV3.DBVER_DW header.version = PwDbHeaderV3.DBVER_DW
@@ -130,7 +130,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try { try {
messageDigest = MessageDigest.getInstance("SHA-256") messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("SHA-256 not implemented here.", e) throw DatabaseOutputException("SHA-256 not implemented here.", e)
} }
// Header checksum // Header checksum
@@ -138,7 +138,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try { try {
headerDigest = MessageDigest.getInstance("SHA-256") headerDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("SHA-256 not implemented here.", e) throw DatabaseOutputException("SHA-256 not implemented here.", e)
} }
var nos = NullOutputStream() var nos = NullOutputStream()
@@ -151,7 +151,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
pho.outputEnd() pho.outputEnd()
headerDos.flush() headerDos.flush()
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} }
val headerHash = headerDigest.digest() val headerHash = headerDigest.digest()
@@ -166,7 +166,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
bos.flush() bos.flush()
bos.close() bos.close()
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Failed to generate checksum.", e) throw DatabaseOutputException("Failed to generate checksum.", e)
} }
header.contentsHash = messageDigest!!.digest() header.contentsHash = messageDigest!!.digest()
@@ -181,14 +181,14 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
pho.outputEnd() pho.outputEnd()
dos.flush() dos.flush()
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} }
return header return header
} }
@Suppress("CAST_NEVER_SUCCEEDS") @Suppress("CAST_NEVER_SUCCEEDS")
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
fun outputPlanGroupAndEntries(os: OutputStream) { fun outputPlanGroupAndEntries(os: OutputStream) {
val los = LEDataOutputStream(os) val los = LEDataOutputStream(os)
@@ -199,7 +199,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
los.writeInt(headerHashBlock!!.size) los.writeInt(headerHashBlock!!.size)
los.write(headerHashBlock!!) los.write(headerHashBlock!!)
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Failed to output header hash.", e) throw DatabaseOutputException("Failed to output header hash.", e)
} }
} }
@@ -209,7 +209,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try { try {
pgo.output() pgo.output()
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Failed to output a tree", e) throw DatabaseOutputException("Failed to output a tree", e)
} }
} }
mDatabaseV3.doForEachEntryInIndex { entry -> mDatabaseV3.doForEachEntryInIndex { entry ->
@@ -217,7 +217,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try { try {
peo.output() peo.output()
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Failed to output an entry.", e) throw DatabaseOutputException("Failed to output an entry.", e)
} }
} }
} }

View File

@@ -29,9 +29,9 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.database.* import com.kunzisoft.keepass.database.*
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.exception.PwDbOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
import com.kunzisoft.keepass.database.file.PwDbHeaderV4 import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.element.security.ProtectedBinary import com.kunzisoft.keepass.database.element.security.ProtectedBinary
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -63,14 +63,14 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
private var headerHmac: ByteArray? = null private var headerHmac: ByteArray? = null
private var engine: CipherEngine? = null private var engine: CipherEngine? = null
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
override fun output() { override fun output() {
try { try {
try { try {
engine = CipherFactory.getInstance(mDatabaseV4.dataCipher) engine = CipherFactory.getInstance(mDatabaseV4.dataCipher)
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("No such cipher", e) throw DatabaseOutputException("No such cipher", e)
} }
header = outputHeader(mOS) header = outputHeader(mOS)
@@ -91,10 +91,9 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
val osXml: OutputStream val osXml: OutputStream
try { try {
if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) { osXml = when(mDatabaseV4.compressionAlgorithm) {
osXml = GZIPOutputStream(osPlain) PwCompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
} else { else -> osPlain
osXml = osPlain
} }
if (header!!.version >= PwDbHeaderV4.FILE_VERSION_32_4) { if (header!!.version >= PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -105,13 +104,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
outputDatabase(osXml) outputDatabase(osXml)
osXml.close() osXml.close()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} }
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException(e) throw DatabaseOutputException(e)
} }
} }
@@ -229,7 +228,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
xml.endTag(null, PwDatabaseV4XML.ElemMeta) xml.endTag(null, PwDatabaseV4XML.ElemMeta)
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
private fun attachStreamEncryptor(header: PwDbHeaderV4, os: OutputStream): CipherOutputStream { private fun attachStreamEncryptor(header: PwDbHeaderV4, os: OutputStream): CipherOutputStream {
val cipher: Cipher val cipher: Cipher
try { try {
@@ -237,13 +236,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseV4.finalKey!!, header.encryptionIV) cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseV4.finalKey!!, header.encryptionIV)
} catch (e: Exception) { } catch (e: Exception) {
throw PwDbOutputException("Invalid algorithm.", e) throw DatabaseOutputException("Invalid algorithm.", e)
} }
return CipherOutputStream(os, cipher) return CipherOutputStream(os, cipher)
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
override fun setIVs(header: PwDbHeaderV4): SecureRandom { override fun setIVs(header: PwDbHeaderV4): SecureRandom {
val random = super.setIVs(header) val random = super.setIVs(header)
random.nextBytes(header.masterSeed) random.nextBytes(header.masterSeed)
@@ -259,7 +258,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
} }
try { try {
val kdf = KdfFactory.getEngineV4(mDatabaseV4.kdfParameters) val kdf = mDatabaseV4.getEngineV4(mDatabaseV4.kdfParameters)
kdf.randomize(mDatabaseV4.kdfParameters!!) kdf.randomize(mDatabaseV4.kdfParameters!!)
} catch (unknownKDF: UnknownKDF) { } catch (unknownKDF: UnknownKDF) {
Log.e(TAG, "Unable to retrieve header", unknownKDF) Log.e(TAG, "Unable to retrieve header", unknownKDF)
@@ -276,7 +275,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
if (randomStream == null) { if (randomStream == null) {
throw PwDbOutputException("Invalid random cipher") throw DatabaseOutputException("Invalid random cipher")
} }
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) { if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -286,7 +285,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
return random return random
} }
@Throws(PwDbOutputException::class) @Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): PwDbHeaderV4 { override fun outputHeader(outputStream: OutputStream): PwDbHeaderV4 {
val header = PwDbHeaderV4(mDatabaseV4) val header = PwDbHeaderV4(mDatabaseV4)
@@ -296,7 +295,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
try { try {
pho.output() pho.output()
} catch (e: IOException) { } catch (e: IOException) {
throw PwDbOutputException("Failed to output the header.", e) throw DatabaseOutputException("Failed to output the header.", e)
} }
hashOfHeader = pho.hashOfHeader hashOfHeader = pho.hashOfHeader
@@ -403,7 +402,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
} }
} else { } else {
if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.Gzip) { if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.GZip) {
xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue); xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue);
@@ -445,7 +444,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
xml.text(String(Base64Coder.encode(encoded))) xml.text(String(Base64Coder.encode(encoded)))
} else { } else {
if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) { if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.GZip) {
xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue) xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue)
val compressData = MemoryUtil.compress(buffer) val compressData = MemoryUtil.compress(buffer)

View File

@@ -66,7 +66,6 @@ open class Education(val activity: Activity) {
val educationResourcesKeys = intArrayOf( val educationResourcesKeys = intArrayOf(
R.string.education_create_db_key, R.string.education_create_db_key,
R.string.education_select_db_key, R.string.education_select_db_key,
R.string.education_open_link_db_key,
R.string.education_unlock_key, R.string.education_unlock_key,
R.string.education_read_only_key, R.string.education_read_only_key,
R.string.education_fingerprint_key, R.string.education_fingerprint_key,
@@ -122,18 +121,6 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_select_db_default)) context.resources.getBoolean(R.bool.education_select_db_default))
} }
/**
* Determines whether the explanatory view of the database selection has already been displayed.
*
* @param context The context to open the SharedPreferences
* @return boolean value of education_select_db_key key
*/
fun isEducationOpenLinkDatabasePerformed(context: Context): Boolean {
val prefs = getEducationSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.education_open_link_db_key),
context.resources.getBoolean(R.bool.education_open_link_db_default))
}
/** /**
* Determines whether the explanatory view of the database unlock has already been displayed. * Determines whether the explanatory view of the database unlock has already been displayed.
* *

View File

@@ -72,31 +72,4 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
}, },
R.string.education_select_db_key) R.string.education_select_db_key)
} }
fun checkAndPerformedOpenLinkDatabaseEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationOpenLinkDatabasePerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_open_link_database_title),
activity.getString(R.string.education_open_link_database_summary))
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_link_white_24dp))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
super.onTargetClick(view)
onEducationViewClick?.invoke(view)
}
override fun onOuterCircleClick(view: TapTargetView?) {
super.onOuterCircleClick(view)
view?.dismiss(false)
onOuterViewClick?.invoke(view)
}
},
R.string.education_open_link_db_key)
}
} }

View File

@@ -83,11 +83,16 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
} }
} }
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply { findPreference<Preference>(getString(R.string.settings_database_security_key))?.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener { onPreferenceClickListener = Preference.OnPreferenceClickListener {
fragmentManager?.let { fragmentManager -> mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_SECURITY)
AssignMasterKeyDialogFragment().show(fragmentManager, "passwordDialog") false
} }
}
findPreference<Preference>(getString(R.string.settings_database_credentials_key))?.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_MASTER_KEY)
false false
} }
} }

View File

@@ -22,36 +22,37 @@ package com.kunzisoft.keepass.settings
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.fragment.app.DialogFragment
import androidx.preference.* import androidx.preference.*
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.KeyboardExplanationDialogFragment import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.settings.preference.DialogListExplanationPreference import com.kunzisoft.keepass.settings.preference.*
import com.kunzisoft.keepass.settings.preference.IconPackListPreference import com.kunzisoft.keepass.settings.preference.DialogColorPreference.Companion.DISABLE_COLOR
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
import com.kunzisoft.keepass.settings.preference.InputTextPreference
import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.settings.preferencedialogfragment.*
class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
@@ -61,12 +62,13 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
private var mCount = 0 private var mCount = 0
private var mRoundPref: InputNumberPreference? = null private var databaseCustomColorPref: DialogColorPreference? = null
private var mMemoryPref: InputNumberPreference? = null private var mRoundPref: InputKdfNumberPreference? = null
private var mParallelismPref: InputNumberPreference? = null private var mMemoryPref: InputKdfNumberPreference? = null
private var mParallelismPref: InputKdfNumberPreference? = null
enum class Screen { enum class Screen {
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, DATABASE, APPEARANCE APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
} }
override fun onResume() { override fun onResume() {
@@ -74,7 +76,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
activity?.let { activity -> activity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillEnablePreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.settings_autofill_enable_key)) val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
if (autoFillEnablePreference != null) { if (autoFillEnablePreference != null) {
val autofillManager = activity.getSystemService(AutofillManager::class.java) val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null autoFillEnablePreference.isChecked = autofillManager != null
@@ -110,6 +112,12 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
Screen.DATABASE -> { Screen.DATABASE -> {
onCreateDatabasePreference(rootKey) onCreateDatabasePreference(rootKey)
} }
Screen.DATABASE_SECURITY -> {
onCreateDatabaseSecurityPreference(rootKey)
}
Screen.DATABASE_MASTER_KEY -> {
onCreateDatabaseMasterKeyPreference(rootKey)
}
} }
} }
@@ -139,7 +147,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
setPreferencesFromResource(R.xml.preferences_form_filling, rootKey) setPreferencesFromResource(R.xml.preferences_form_filling, rootKey)
activity?.let { activity -> activity?.let { activity ->
val autoFillEnablePreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.settings_autofill_enable_key)) val autoFillEnablePreference: SwitchPreference? = findPreference(getString(R.string.settings_autofill_enable_key))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autofillManager = activity.getSystemService(AutofillManager::class.java) val autofillManager = activity.getSystemService(AutofillManager::class.java)
if (autofillManager != null && autofillManager.hasEnabledAutofillServices()) if (autofillManager != null && autofillManager.hasEnabledAutofillServices())
@@ -217,7 +225,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey) setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
activity?.let { activity -> activity?.let { activity ->
val biometricUnlockEnablePreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.biometric_unlock_enable_key)) val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
// < M solve verifyError exception // < M solve verifyError exception
var biometricUnlockSupported = false var biometricUnlockSupported = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -240,7 +248,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
} }
} }
val deleteKeysFingerprints: Preference? = findPreference<Preference>(getString(R.string.biometric_delete_all_key_key)) val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
if (!biometricUnlockSupported) { if (!biometricUnlockSupported) {
deleteKeysFingerprints?.isEnabled = false deleteKeysFingerprints?.isEnabled = false
} else { } else {
@@ -338,26 +346,54 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
if (mDatabase.loaded) { if (mDatabase.loaded) {
val dbGeneralPrefCategory: PreferenceCategory? = findPreference<PreferenceCategory>(getString(R.string.database_general_key)) val dbGeneralPrefCategory: PreferenceCategory? = findPreference(getString(R.string.database_general_key))
// Db name // Database name
val dbNamePref: InputTextPreference? = findPreference<InputTextPreference>(getString(R.string.database_name_key)) val dbNamePref: InputTextPreference? = findPreference(getString(R.string.database_name_key))
if (mDatabase.containsName()) { if (mDatabase.containsName()) {
dbNamePref?.summary = mDatabase.name dbNamePref?.summary = mDatabase.name
} else { } else {
dbGeneralPrefCategory?.removePreference(dbNamePref) dbGeneralPrefCategory?.removePreference(dbNamePref)
} }
// Db description // Database description
val dbDescriptionPref: InputTextPreference? = findPreference<InputTextPreference>(getString(R.string.database_description_key)) val dbDescriptionPref: InputTextPreference? = findPreference(getString(R.string.database_description_key))
if (mDatabase.containsDescription()) { if (mDatabase.containsDescription()) {
dbDescriptionPref?.summary = mDatabase.description dbDescriptionPref?.summary = mDatabase.description
} else { } else {
dbGeneralPrefCategory?.removePreference(dbDescriptionPref) dbGeneralPrefCategory?.removePreference(dbDescriptionPref)
} }
// Database default username
val dbDefaultUsername: InputTextPreference? = findPreference(getString(R.string.database_default_username_key))
if (mDatabase.containsDefaultUsername()) {
dbDefaultUsername?.summary = mDatabase.defaultUsername
} else {
dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
}
// Database custom color
databaseCustomColorPref = findPreference(getString(R.string.database_custom_color_key))
if (mDatabase.containsCustomColor()) {
databaseCustomColorPref?.apply {
try {
color = Color.parseColor(mDatabase.color)
summary = mDatabase.color
} catch (e: Exception) {
color = DISABLE_COLOR
summary = ""
}
}
} else {
dbGeneralPrefCategory?.removePreference(databaseCustomColorPref)
}
// Database compression
findPreference<Preference>(getString(R.string.database_data_compression_key))
?.summary = (mDatabase.compressionAlgorithm ?: PwCompressionAlgorithm.None).getName(resources)
// Recycle bin // Recycle bin
val recycleBinPref: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.recycle_bin_key)) val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key))
// TODO Recycle // TODO Recycle
dbGeneralPrefCategory?.removePreference(recycleBinPref) // To delete dbGeneralPrefCategory?.removePreference(recycleBinPref) // To delete
if (mDatabase.isRecycleBinAvailable) { if (mDatabase.isRecycleBinAvailable) {
@@ -371,6 +407,26 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
findPreference<Preference>(getString(R.string.database_version_key)) findPreference<Preference>(getString(R.string.database_version_key))
?.summary = mDatabase.getVersion() ?.summary = mDatabase.getVersion()
findPreference<PreferenceCategory>(getString(R.string.database_history_key))
?.isVisible = mDatabase.manageHistory == true
// Max history items
findPreference<InputNumberPreference>(getString(R.string.max_history_items_key))
?.summary = mDatabase.historyMaxItems.toString()
// Max history size
findPreference<InputNumberPreference>(getString(R.string.max_history_size_key))
?.summary = mDatabase.historyMaxSize.toString()
} else {
Log.e(javaClass.name, "Database isn't ready")
}
}
private fun onCreateDatabaseSecurityPreference(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_database_security, rootKey)
if (mDatabase.loaded) {
// Encryption Algorithm // Encryption Algorithm
findPreference<DialogListExplanationPreference>(getString(R.string.encryption_algorithm_key)) findPreference<DialogListExplanationPreference>(getString(R.string.encryption_algorithm_key))
?.summary = mDatabase.getEncryptionAlgorithmName(resources) ?.summary = mDatabase.getEncryptionAlgorithmName(resources)
@@ -380,24 +436,41 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
?.summary = mDatabase.getKeyDerivationName(resources) ?.summary = mDatabase.getKeyDerivationName(resources)
// Round encryption // Round encryption
mRoundPref = findPreference<InputNumberPreference>(getString(R.string.transform_rounds_key)) mRoundPref = findPreference(getString(R.string.transform_rounds_key))
mRoundPref?.summary = mDatabase.numberKeyEncryptionRoundsAsString mRoundPref?.summary = mDatabase.numberKeyEncryptionRounds.toString()
// Memory Usage // Memory Usage
mMemoryPref = findPreference<InputNumberPreference>(getString(R.string.memory_usage_key)) mMemoryPref = findPreference(getString(R.string.memory_usage_key))
mMemoryPref?.summary = mDatabase.memoryUsageAsString mMemoryPref?.summary = mDatabase.memoryUsage.toString()
// Parallelism // Parallelism
mParallelismPref = findPreference<InputNumberPreference>(getString(R.string.parallelism_key)) mParallelismPref = findPreference(getString(R.string.parallelism_key))
mParallelismPref?.summary = mDatabase.parallelismAsString mParallelismPref?.summary = mDatabase.parallelism.toString()
} else {
Log.e(javaClass.name, "Database isn't ready")
}
}
private fun onCreateDatabaseMasterKeyPreference(rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_database_master_key, rootKey)
if (mDatabase.loaded) {
findPreference<Preference>(getString(R.string.settings_database_change_credentials_key))?.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
fragmentManager?.let { fragmentManager ->
AssignMasterKeyDialogFragment.getInstance(mDatabase.allowNoMasterKey())
.show(fragmentManager, "passwordDialog")
}
false
}
}
} else { } else {
Log.e(javaClass.name, "Database isn't ready") Log.e(javaClass.name, "Database isn't ready")
} }
} }
private fun allowCopyPassword() { private fun allowCopyPassword() {
val copyPasswordPreference: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.allow_copy_password_key)) val copyPasswordPreference: SwitchPreference? = findPreference(getString(R.string.allow_copy_password_key))
copyPasswordPreference?.setOnPreferenceChangeListener { _, newValue -> copyPasswordPreference?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean && context != null) { if (newValue as Boolean && context != null) {
val message = getString(R.string.allow_copy_password_warning) + val message = getString(R.string.allow_copy_password_warning) +
@@ -447,6 +520,27 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
} }
} }
private val colorSelectedListener: ((Boolean, Int)-> Unit)? = { enable, color ->
databaseCustomColorPref?.summary = ChromaUtil.getFormattedColorString(color, false)
if (enable) {
databaseCustomColorPref?.color = color
} else {
databaseCustomColorPref?.color = DISABLE_COLOR
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
try {
// To reassign color listener after orientation change
val chromaDialog = fragmentManager?.findFragmentByTag(TAG_PREF_FRAGMENT) as DatabaseColorPreferenceDialogFragmentCompat?
chromaDialog?.onColorSelectedListener = colorSelectedListener
} catch (e: Exception) {}
return view
}
override fun onDisplayPreferenceDialog(preference: Preference?) { override fun onDisplayPreferenceDialog(preference: Preference?) {
var otherDialogFragment = false var otherDialogFragment = false
@@ -461,6 +555,23 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
preference.key == getString(R.string.database_description_key) -> { preference.key == getString(R.string.database_description_key) -> {
dialogFragment = DatabaseDescriptionPreferenceDialogFragmentCompat.newInstance(preference.key) dialogFragment = DatabaseDescriptionPreferenceDialogFragmentCompat.newInstance(preference.key)
} }
preference.key == getString(R.string.database_default_username_key) -> {
dialogFragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat.newInstance(preference.key)
}
preference.key == getString(R.string.database_custom_color_key) -> {
dialogFragment = DatabaseColorPreferenceDialogFragmentCompat.newInstance(preference.key).apply {
onColorSelectedListener = colorSelectedListener
}
}
preference.key == getString(R.string.database_data_compression_key) -> {
dialogFragment = DatabaseDataCompressionPreferenceDialogFragmentCompat.newInstance(preference.key)
}
preference.key == getString(R.string.max_history_items_key) -> {
dialogFragment = MaxHistoryItemsPreferenceDialogFragmentCompat.newInstance(preference.key)
}
preference.key == getString(R.string.max_history_size_key) -> {
dialogFragment = MaxHistorySizePreferenceDialogFragmentCompat.newInstance(preference.key)
}
preference.key == getString(R.string.encryption_algorithm_key) -> { preference.key == getString(R.string.encryption_algorithm_key) -> {
dialogFragment = DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.newInstance(preference.key) dialogFragment = DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.newInstance(preference.key)
} }
@@ -486,7 +597,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
if (dialogFragment != null && !mDatabaseReadOnly) { if (dialogFragment != null && !mDatabaseReadOnly) {
dialogFragment.setTargetFragment(this, 0) dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(fragmentManager, null) dialogFragment.show(fragmentManager, TAG_PREF_FRAGMENT)
} }
// Could not be handled here. Try with the super method. // Could not be handled here. Try with the super method.
else if (otherDialogFragment) { else if (otherDialogFragment) {
@@ -510,6 +621,8 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
private const val TAG_KEY = "NESTED_KEY" private const val TAG_KEY = "NESTED_KEY"
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
private const val REQUEST_CODE_AUTOFILL = 5201 private const val REQUEST_CODE_AUTOFILL = 5201
@JvmOverloads @JvmOverloads
@@ -529,8 +642,10 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings) Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings) Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings)
Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings) Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings)
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings) Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings)
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings)
Screen.DATABASE_MASTER_KEY -> resources.getString(R.string.menu_master_key_settings)
} }
} }
} }

View File

@@ -33,6 +33,12 @@ object PreferencesUtil {
return prefs.getBoolean(context.getString(R.string.show_read_only_warning), true) return prefs.getBoolean(context.getString(R.string.show_read_only_warning), true)
} }
fun rememberKeyFiles(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.keyfile_key),
context.resources.getBoolean(R.bool.keyfile_default))
}
fun omitBackup(context: Context): Boolean { fun omitBackup(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.omitbackup_key), return prefs.getBoolean(context.getString(R.string.omitbackup_key),
@@ -109,8 +115,8 @@ object PreferencesUtil {
fun getAppTimeout(context: Context): Long { fun getAppTimeout(context: Context): Long {
return try { return try {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
java.lang.Long.parseLong(prefs.getString(context.getString(R.string.app_timeout_key), (prefs.getString(context.getString(R.string.app_timeout_key),
context.getString(R.string.clipboard_timeout_default)) ?: "60000") context.getString(R.string.clipboard_timeout_default)) ?: "300000").toLong()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
TimeoutHelper.DEFAULT_TIMEOUT TimeoutHelper.DEFAULT_TIMEOUT
} }
@@ -185,12 +191,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.monospace_font_fields_enable_default)) context.resources.getBoolean(R.bool.monospace_font_fields_enable_default))
} }
fun autoOpenSelectedFile(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_open_file_uri_key),
context.resources.getBoolean(R.bool.auto_open_file_uri_default))
}
fun isFirstTimeAskAllowCopyPasswordAndProtectedFields(context: Context): Boolean { fun isFirstTimeAskAllowCopyPasswordAndProtectedFields(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key), return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key),

View File

@@ -115,7 +115,7 @@ open class SettingsActivity
true) true)
} }
// Show the progress dialog now or after dialog confirmation // Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword)) { if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
progressDialogThread.start() progressDialogThread.start()
} else { } else {
PasswordEncodingDialogFragment().apply { PasswordEncodingDialogFragment().apply {

View File

@@ -0,0 +1,33 @@
package com.kunzisoft.keepass.settings.preference
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import androidx.annotation.ColorInt
import com.kunzisoft.androidclearchroma.ChromaPreferenceCompat
import com.kunzisoft.keepass.R
class DialogColorPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence?) {
if (color == DISABLE_COLOR)
super.setSummary("")
else
super.setSummary(summary)
}
override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_input_color
}
companion object {
@ColorInt
const val DISABLE_COLOR: Int = Color.TRANSPARENT
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputTextPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
if (summary == UNKNOWN_VALUE_STRING) {
isEnabled = false
super.setSummary("")
} else {
isEnabled = true
super.setSummary(summary)
}
}
companion object {
const val UNKNOWN_VALUE_STRING = KdfEngine.UNKNOWN_VALUE.toString()
}
}

View File

@@ -20,66 +20,28 @@
package com.kunzisoft.keepass.settings.preference package com.kunzisoft.keepass.settings.preference
import android.content.Context import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet import android.util.AttributeSet
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
class InputNumberPreference @JvmOverloads constructor(context: Context, open class InputNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputTextExplanationPreference(context, attrs, defStyleAttr, defStyleRes) { : InputTextPreference(context, attrs, defStyleAttr, defStyleRes) {
// Save to Shared Preferences
var number: Long = 0
set(number) {
field = number
persistLong(number)
}
override fun getDialogLayoutResource(): Int { override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_numbers return R.layout.pref_dialog_input_numbers
} }
override fun setSummary(summary: CharSequence) { override fun setSummary(summary: CharSequence) {
if (summary == KdfEngine.UNKNOWN_VALUE_STRING) { if (summary == INFINITE_VALUE_STRING) {
isEnabled = false
super.setSummary("") super.setSummary("")
} else { } else {
isEnabled = true
super.setSummary(summary) super.setSummary(summary)
} }
} }
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any { companion object {
// Default value from attribute. Fallback value is set to 0. const val INFINITE_VALUE_STRING = "-1"
return a?.getInt(index, 0) ?: 0
} }
override fun onSetInitialValue(restorePersistedValue: Boolean,
defaultValue: Any?) {
// Read the value. Use the default value if it is not possible.
var numberValue: Long
if (!restorePersistedValue) {
numberValue = 100000
if (defaultValue is String) {
numberValue = java.lang.Long.parseLong(defaultValue)
}
if (defaultValue is Int) {
numberValue = defaultValue.toLong()
}
try {
numberValue = defaultValue as Long
} catch (e: Exception) {
e.printStackTrace()
}
} else {
numberValue = getPersistedLong(this.number)
}
number = numberValue
}
} }

View File

@@ -1,32 +0,0 @@
package com.kunzisoft.keepass.settings.preference
import android.content.Context
import androidx.preference.DialogPreference
import android.util.AttributeSet
import com.kunzisoft.keepass.R
open class InputTextExplanationPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
var explanation: String? = null
init {
val styleAttributes = context.theme.obtainStyledAttributes(
attrs,
R.styleable.explanationDialog,
0, 0)
try {
explanation = styleAttributes.getString(R.styleable.explanationDialog_explanations)
} finally {
styleAttributes.recycle()
}
}
override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_input_text_explanation
}
}

View File

@@ -6,10 +6,10 @@ import android.util.AttributeSet
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
class InputTextPreference @JvmOverloads constructor(context: Context, open class InputTextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun getDialogLayoutResource(): Int { override fun getDialogLayoutResource(): Int {

View File

@@ -0,0 +1,171 @@
/*
* 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.settings.preferencedialogfragment
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.CompoundButton
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.androidclearchroma.IndicatorMode
import com.kunzisoft.androidclearchroma.colormode.ColorMode
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment
import com.kunzisoft.androidclearchroma.fragment.ChromaColorFragment.*
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
import java.lang.Exception
class DatabaseColorPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private lateinit var rootView: View
private lateinit var enableSwitchView: CompoundButton
private var chromaColorFragment: ChromaColorFragment? = null
var onColorSelectedListener: ((enable: Boolean, color: Int) -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialogBuilder = AlertDialog.Builder(activity!!)
rootView = activity!!.layoutInflater.inflate(R.layout.pref_dialog_input_color, null)
enableSwitchView = rootView.findViewById(R.id.switch_element)
val fragmentManager = childFragmentManager
chromaColorFragment = fragmentManager.findFragmentByTag(TAG_FRAGMENT_COLORS) as ChromaColorFragment?
val fragmentTransaction = fragmentManager.beginTransaction()
database?.let { database ->
val initColor = try {
enableSwitchView.isChecked = true
Color.parseColor(database.color)
} catch (e: Exception) {
enableSwitchView.isChecked = false
DEFAULT_COLOR
}
arguments?.putInt(ARG_INITIAL_COLOR, initColor)
}
if (chromaColorFragment == null) {
chromaColorFragment = newInstance(arguments)
fragmentTransaction.add(com.kunzisoft.androidclearchroma.R.id.color_dialog_container, chromaColorFragment!!, TAG_FRAGMENT_COLORS).commit()
}
alertDialogBuilder.setPositiveButton(android.R.string.ok) { _, _ ->
val currentColor = chromaColorFragment!!.currentColor
val customColorEnable = enableSwitchView.isChecked
onColorSelectedListener?.invoke(customColorEnable, currentColor)
database?.let { database ->
val newColor = if (customColorEnable) {
ChromaUtil.getFormattedColorString(currentColor, false)
} else {
""
}
val oldColor = database.color
database.color = newColor
actionInUIThreadAfterSaveDatabase = AfterColorSave(newColor, oldColor)
}
super.onDialogClosed(true)
dismiss()
}
alertDialogBuilder.setNegativeButton(android.R.string.cancel) { _, _ ->
super.onDialogClosed(false)
dismiss()
}
alertDialogBuilder.setView(rootView)
val dialog = alertDialogBuilder.create()
// request a window without the title
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
dialog.setOnShowListener { measureLayout(it as Dialog) }
return dialog
}
/**
* Set new dimensions to dialog
* @param ad dialog
*/
private fun measureLayout(ad: Dialog) {
val typedValue = TypedValue()
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_height_multiplier, typedValue, true)
val heightMultiplier = typedValue.float
val height = (ad.context.resources.displayMetrics.heightPixels * heightMultiplier).toInt()
resources.getValue(com.kunzisoft.androidclearchroma.R.dimen.chroma_dialog_width_multiplier, typedValue, true)
val widthMultiplier = typedValue.float
val width = (ad.context.resources.displayMetrics.widthPixels * widthMultiplier).toInt()
ad.window?.setLayout(width, height)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return rootView
}
private inner class AfterColorSave(private val mNewColor: String,
private val mOldColor: String)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
val defaultColorToShow =
if (result.isSuccess) {
mNewColor
} else {
database?.color = mOldColor
mOldColor
}
preference.summary = defaultColorToShow
}
}
companion object {
private const val TAG_FRAGMENT_COLORS = "TAG_FRAGMENT_COLORS"
@ColorInt
const val DEFAULT_COLOR: Int = Color.WHITE
fun newInstance(key: String): DatabaseColorPreferenceDialogFragmentCompat {
val fragment = DatabaseColorPreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
bundle.putInt(ARG_INITIAL_COLOR, Color.BLACK)
bundle.putInt(ARG_COLOR_MODE, ColorMode.RGB.ordinal)
bundle.putInt(ARG_INDICATOR_MODE, IndicatorMode.HEX.ordinal)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListRadioItemAdapter
import com.kunzisoft.keepass.tasks.ActionRunnable
class DatabaseDataCompressionPreferenceDialogFragmentCompat
: DatabaseSavePreferenceDialogFragmentCompat(),
ListRadioItemAdapter.RadioItemSelectedCallback<PwCompressionAlgorithm> {
private var compressionSelected: PwCompressionAlgorithm? = null
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
setExplanationText(R.string.database_data_compression_summary)
val recyclerView = view.findViewById<RecyclerView>(R.id.pref_dialog_list)
recyclerView.layoutManager = LinearLayoutManager(context)
activity?.let { activity ->
val compressionAdapter = ListRadioItemAdapter<PwCompressionAlgorithm>(activity)
compressionAdapter.setRadioItemSelectedCallback(this)
recyclerView.adapter = compressionAdapter
database?.let { database ->
compressionSelected = database.compressionAlgorithm?.apply {
compressionAdapter.setItems(database.availableCompressionAlgorithms, this)
}
}
}
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
database?.let { database ->
if (compressionSelected != null) {
val newAlgorithm = compressionSelected
val oldAlgorithm = database.compressionAlgorithm
database.compressionAlgorithm = newAlgorithm
if (oldAlgorithm != null && newAlgorithm != null)
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm)
}
}
}
super.onDialogClosed(positiveResult)
}
override fun onItemSelected(item: PwCompressionAlgorithm) {
this.compressionSelected = item
}
private inner class AfterDescriptionSave(private val mNewAlgorithm: PwCompressionAlgorithm,
private val mOldAlgorithm: PwCompressionAlgorithm)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
val algorithmToShow =
if (result.isSuccess) {
mNewAlgorithm
} else {
database?.compressionAlgorithm = mOldAlgorithm
mOldAlgorithm
}
preference.summary = algorithmToShow.getName(settingsResources)
}
}
companion object {
fun newInstance(key: String): DatabaseDataCompressionPreferenceDialogFragmentCompat {
val fragment = DatabaseDataCompressionPreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.tasks.ActionRunnable
class DatabaseDefaultUsernamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
inputText = database?.defaultUsername?: ""
}
override fun onDialogClosed(positiveResult: Boolean) {
database?.let { database ->
if (positiveResult) {
val newDefaultUsername = inputText
val oldDefaultUsername = database.defaultUsername
database.defaultUsername = newDefaultUsername
actionInUIThreadAfterSaveDatabase = AfterDefaultUsernameSave(newDefaultUsername, oldDefaultUsername)
}
}
super.onDialogClosed(positiveResult)
}
private inner class AfterDefaultUsernameSave(private val mNewDefaultUsername: String,
private val mOldDefaultUsername: String)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
val defaultUsernameToShow =
if (result.isSuccess) {
mNewDefaultUsername
} else {
database?.defaultUsername = mOldDefaultUsername
mOldDefaultUsername
}
preference.summary = defaultUsernameToShow
}
}
companion object {
fun newInstance(key: String): DatabaseDefaultUsernamePreferenceDialogFragmentCompat {
val fragment = DatabaseDefaultUsernamePreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -23,7 +23,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { class DatabaseDescriptionPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
@@ -32,12 +32,14 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePrefe
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult) { database?.let { database ->
val newDescription = inputText if (positiveResult) {
val oldDescription = database!!.description val newDescription = inputText
database?.assignDescription(newDescription) val oldDescription = database.description
database.description = newDescription
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription) actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newDescription, oldDescription)
}
} }
super.onDialogClosed(positiveResult) super.onDialogClosed(positiveResult)
@@ -48,10 +50,13 @@ class DatabaseDescriptionPreferenceDialogFragmentCompat : InputDatabaseSavePrefe
: ActionRunnable() { : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
val descriptionToShow = mNewDescription val descriptionToShow =
if (!result.isSuccess) { if (result.isSuccess) {
database?.assignDescription(mOldDescription) mNewDescription
} } else {
database?.description = mOldDescription
mOldDescription
}
preference.summary = descriptionToShow preference.summary = descriptionToShow
} }
} }

View File

@@ -56,17 +56,19 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult && database!!.allowEncryptionAlgorithmModification()) {
if (algorithmSelected != null) { if (positiveResult) {
val newAlgorithm = algorithmSelected database?.let { database ->
val oldAlgorithm = database?.encryptionAlgorithm if (database.allowEncryptionAlgorithmModification()) {
newAlgorithm?.let { if (algorithmSelected != null) {
database?.assignEncryptionAlgorithm(it) val newAlgorithm = algorithmSelected
val oldAlgorithm = database.encryptionAlgorithm
database.encryptionAlgorithm = newAlgorithm
if (oldAlgorithm != null && newAlgorithm != null)
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm)
}
} }
if (oldAlgorithm != null && newAlgorithm != null)
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newAlgorithm, oldAlgorithm)
} }
} }
@@ -82,11 +84,13 @@ class DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat
: ActionRunnable() { : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
var algorithmToShow = mNewAlgorithm val algorithmToShow =
if (!result.isSuccess) { if (result.isSuccess) {
database?.assignEncryptionAlgorithm(mOldAlgorithm) mNewAlgorithm
algorithmToShow = mOldAlgorithm } else {
} database?.encryptionAlgorithm = mOldAlgorithm
mOldAlgorithm
}
preference.summary = algorithmToShow.getName(settingsResources) preference.summary = algorithmToShow.getName(settingsResources)
} }
} }

View File

@@ -52,21 +52,24 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
recyclerView.adapter = kdfAdapter recyclerView.adapter = kdfAdapter
database?.let { database -> database?.let { database ->
kdfEngineSelected = database.kdfEngine kdfEngineSelected = database.kdfEngine?.apply {
if (kdfEngineSelected != null) kdfAdapter.setItems(database.availableKdfEngines, this)
kdfAdapter.setItems(database.availableKdfEngines, kdfEngineSelected!!) }
} }
} }
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult && database!!.allowKdfModification()) { if (positiveResult) {
if (kdfEngineSelected != null) { database?.let { database ->
val newKdfEngine = kdfEngineSelected!! if (database.allowKdfModification()) {
val oldKdfEngine = database!!.kdfEngine val newKdfEngine = kdfEngineSelected
database?.assignKdfEngine(newKdfEngine) val oldKdfEngine = database.kdfEngine
if (newKdfEngine != null && oldKdfEngine != null) {
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newKdfEngine, oldKdfEngine) database.kdfEngine = newKdfEngine
actionInUIThreadAfterSaveDatabase = AfterDescriptionSave(newKdfEngine, oldKdfEngine)
}
}
} }
} }
@@ -94,17 +97,19 @@ class DatabaseKeyDerivationPreferenceDialogFragmentCompat
: ActionRunnable() { : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
val kdfEngineToShow = mNewKdfEngine val kdfEngineToShow =
if (result.isSuccess) {
if (!result.isSuccess) { mNewKdfEngine
database?.assignKdfEngine(mOldKdfEngine) } else {
} database?.kdfEngine = mOldKdfEngine
mOldKdfEngine
}
preference.summary = kdfEngineToShow.getName(settingsResources) preference.summary = kdfEngineToShow.getName(settingsResources)
roundPreference?.summary = kdfEngineToShow.defaultKeyRounds.toString() roundPreference?.summary = kdfEngineToShow.defaultKeyRounds.toString()
// Disable memory and parallelism if not available // Disable memory and parallelism if not available
memoryPreference?.summary = kdfEngineToShow.getDefaultMemoryUsage().toString() memoryPreference?.summary = kdfEngineToShow.defaultMemoryUsage.toString()
parallelismPreference?.summary = kdfEngineToShow.getDefaultParallelism().toString() parallelismPreference?.summary = kdfEngineToShow.defaultParallelism.toString()
} }
} }

View File

@@ -23,7 +23,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { class DatabaseNamePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
@@ -32,12 +32,14 @@ class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDi
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult) { if (positiveResult) {
val newName = inputText database?.let { database ->
val oldName = database!!.name val newName = inputText
database?.assignName(newName) val oldName = database.name
database.name = newName
actionInUIThreadAfterSaveDatabase = AfterNameSave(newName, oldName) actionInUIThreadAfterSaveDatabase = AfterNameSave(newName, oldName)
}
} }
super.onDialogClosed(positiveResult) super.onDialogClosed(positiveResult)
@@ -48,10 +50,13 @@ class DatabaseNamePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDi
: ActionRunnable() { : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
val nameToShow = mNewName val nameToShow =
if (!result.isSuccess) { if (result.isSuccess) {
database?.assignName(mOldName) mNewName
} } else {
database?.name = mOldName
mOldName
}
preference.summary = nameToShow preference.summary = nameToShow
} }
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.settings.preferencedialogfragment package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.res.Resources import android.content.res.Resources
import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
@@ -36,10 +37,14 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
protected lateinit var settingsResources: Resources protected lateinit var settingsResources: Resources
override fun onBindDialogView(view: View) { override fun onCreate(savedInstanceState: Bundle?) {
super.onBindDialogView(view) super.onCreate(savedInstanceState)
this.database = Database.getInstance() this.database = Database.getInstance()
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
activity?.resources?.let { settingsResources = it } activity?.resources?.let { settingsResources = it }
} }

View File

@@ -1,45 +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.settings.preferencedialogfragment
import android.view.View
import android.widget.EditText
import com.kunzisoft.keepass.R
open class InputDatabaseSavePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var inputTextView: EditText? = null
var inputText: String
get() = this.inputTextView?.text?.toString() ?: ""
set(inputText) {
if (inputTextView != null) {
this.inputTextView?.setText(inputText)
this.inputTextView?.setSelection(this.inputTextView!!.text.length)
}
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
inputTextView = view.findViewById(R.id.input_text)
}
}

View File

@@ -19,36 +19,71 @@
*/ */
package com.kunzisoft.keepass.settings.preferencedialogfragment package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.view.View
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.preference.PreferenceDialogFragmentCompat import androidx.preference.PreferenceDialogFragmentCompat
import android.view.View
import android.widget.TextView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
private var inputTextView: EditText? = null
private var textExplanationView: TextView? = null private var textExplanationView: TextView? = null
private var switchElementView: CompoundButton? = null
var inputText: String
get() = this.inputTextView?.text?.toString() ?: ""
set(inputText) {
if (inputTextView != null) {
this.inputTextView?.setText(inputText)
this.inputTextView?.setSelection(this.inputTextView!!.text.length)
}
}
var explanationText: String? var explanationText: String?
get() = textExplanationView?.text?.toString() ?: "" get() = textExplanationView?.text?.toString() ?: ""
set(explanationText) { set(explanationText) {
if (explanationText != null && explanationText.isNotEmpty()) { textExplanationView?.apply {
textExplanationView?.text = explanationText if (explanationText != null && explanationText.isNotEmpty()) {
textExplanationView?.visibility = View.VISIBLE text = explanationText
} else { visibility = View.VISIBLE
textExplanationView?.text = explanationText } else {
textExplanationView?.visibility = View.VISIBLE text = ""
visibility = View.GONE
}
} }
} }
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
inputTextView = view.findViewById(R.id.input_text)
textExplanationView = view.findViewById(R.id.explanation_text) textExplanationView = view.findViewById(R.id.explanation_text)
textExplanationView?.visibility = View.GONE
switchElementView = view.findViewById(R.id.switch_element)
switchElementView?.visibility = View.GONE
}
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
} }
fun setExplanationText(@StringRes explanationTextId: Int) { fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId) explanationText = getString(explanationTextId)
} }
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
switchElementView?.isChecked = defaultChecked
inputTextView?.visibility = if (defaultChecked) View.VISIBLE else View.GONE
switchElementView?.setOnCheckedChangeListener { _, isChecked ->
onCheckedChange?.invoke(isChecked)
}
}
} }

View File

@@ -0,0 +1,97 @@
/*
* 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.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
class MaxHistoryItemsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
setExplanationText(R.string.max_history_items_summary)
database?.historyMaxItems?.let { maxItemsDatabase ->
inputText = maxItemsDatabase.toString()
setSwitchAction({ isChecked ->
inputText = if (!isChecked) {
INFINITE_MAX_HISTORY_ITEMS.toString()
} else
DEFAULT_MAX_HISTORY_ITEMS.toString()
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_ITEMS)
}
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
database?.let { database ->
var maxHistoryItems: Int = try {
inputText.toInt()
} catch (e: NumberFormatException) {
DEFAULT_MAX_HISTORY_ITEMS
}
if (maxHistoryItems < INFINITE_MAX_HISTORY_ITEMS) {
maxHistoryItems = INFINITE_MAX_HISTORY_ITEMS
}
val oldMaxHistoryItems = database.historyMaxItems
database.historyMaxItems = maxHistoryItems
actionInUIThreadAfterSaveDatabase = AfterMaxHistoryItemsSave(maxHistoryItems, oldMaxHistoryItems)
}
}
super.onDialogClosed(positiveResult)
}
private inner class AfterMaxHistoryItemsSave(private val mNewMaxHistoryItems: Int,
private val mOldMaxHistoryItems: Int)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
val maxHistoryItemsToShow =
if (result.isSuccess) {
mNewMaxHistoryItems
} else {
database?.historyMaxItems = mOldMaxHistoryItems
mOldMaxHistoryItems
}
preference.summary = maxHistoryItemsToShow.toString()
}
}
companion object {
const val DEFAULT_MAX_HISTORY_ITEMS = 10
const val INFINITE_MAX_HISTORY_ITEMS = -1
fun newInstance(key: String): MaxHistoryItemsPreferenceDialogFragmentCompat {
val fragment = MaxHistoryItemsPreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
setExplanationText(R.string.max_history_size_summary)
database?.historyMaxSize?.let { maxItemsDatabase ->
inputText = maxItemsDatabase.toString()
setSwitchAction({ isChecked ->
inputText = if (!isChecked) {
INFINITE_MAX_HISTORY_SIZE.toString()
} else
DEFAULT_MAX_HISTORY_SIZE.toString()
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
}
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
database?.let { database ->
var maxHistorySize: Long = try {
inputText.toLong()
} catch (e: NumberFormatException) {
DEFAULT_MAX_HISTORY_SIZE
}
if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) {
maxHistorySize = INFINITE_MAX_HISTORY_SIZE
}
val oldMaxHistorySize = database.historyMaxSize
database.historyMaxSize = maxHistorySize
actionInUIThreadAfterSaveDatabase = AfterMaxHistorySizeSave(maxHistorySize, oldMaxHistorySize)
}
}
super.onDialogClosed(positiveResult)
}
private inner class AfterMaxHistorySizeSave(private val mNewMaxHistorySize: Long,
private val mOldMaxHistorySize: Long)
: ActionRunnable() {
override fun onFinishRun(result: Result) {
val maxHistorySizeToShow =
if (result.isSuccess) {
mNewMaxHistorySize
} else {
database?.historyMaxSize = mOldMaxHistorySize
mOldMaxHistorySize
}
preference.summary = maxHistorySizeToShow.toString()
}
}
companion object {
const val DEFAULT_MAX_HISTORY_SIZE = 134217728L
const val INFINITE_MAX_HISTORY_SIZE = -1L
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -21,38 +21,36 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
setExplanationText(R.string.memory_usage_explanation) setExplanationText(R.string.memory_usage_explanation)
inputText = database?.memoryUsageAsString ?: "" inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult) { if (positiveResult) {
var memoryUsage: Long database?.let { database ->
try { var memoryUsage: Long = try {
val stringMemory = inputText inputText.toLong()
memoryUsage = java.lang.Long.parseLong(stringMemory) } catch (e: NumberFormatException) {
} catch (e: NumberFormatException) { MIN_MEMORY_USAGE
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error }
return if (memoryUsage < MIN_MEMORY_USAGE) {
memoryUsage = MIN_MEMORY_USAGE
}
// TODO Max Memory
val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage
actionInUIThreadAfterSaveDatabase = AfterMemorySave(memoryUsage, oldMemoryUsage)
} }
if (memoryUsage < 1) {
memoryUsage = 1
}
val oldMemoryUsage = database!!.memoryUsage
database!!.memoryUsage = memoryUsage
actionInUIThreadAfterSaveDatabase = AfterMemorySave(memoryUsage, oldMemoryUsage)
} }
super.onDialogClosed(positiveResult) super.onDialogClosed(positiveResult)
@@ -63,16 +61,21 @@ class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDia
: ActionRunnable() { : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
val memoryToShow = mNewMemory val memoryToShow =
if (!result.isSuccess) { if (result.isSuccess) {
database?.memoryUsage = mOldMemory mNewMemory
} } else {
database?.memoryUsage = mOldMemory
mOldMemory
}
preference.summary = memoryToShow.toString() preference.summary = memoryToShow.toString()
} }
} }
companion object { companion object {
const val MIN_MEMORY_USAGE = 1L
fun newInstance(key: String): MemoryUsagePreferenceDialogFragmentCompat { fun newInstance(key: String): MemoryUsagePreferenceDialogFragmentCompat {
val fragment = MemoryUsagePreferenceDialogFragmentCompat() val fragment = MemoryUsagePreferenceDialogFragmentCompat()
val bundle = Bundle(1) val bundle = Bundle(1)

View File

@@ -21,38 +21,36 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
class ParallelismPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { class ParallelismPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
setExplanationText(R.string.parallelism_explanation) setExplanationText(R.string.parallelism_explanation)
inputText = database?.parallelismAsString ?: "" inputText = database?.parallelism?.toString() ?: MIN_PARALLELISM.toString()
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult) { if (positiveResult) {
var parallelism: Int database?.let { database ->
try { var parallelism: Int = try {
val stringParallelism = inputText inputText.toInt()
parallelism = Integer.parseInt(stringParallelism) } catch (e: NumberFormatException) {
} catch (e: NumberFormatException) { MIN_PARALLELISM
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error }
return if (parallelism < MIN_PARALLELISM) {
parallelism = MIN_PARALLELISM
}
// TODO Max Parallelism
val oldParallelism = database.parallelism
database.parallelism = parallelism
actionInUIThreadAfterSaveDatabase = AfterParallelismSave(parallelism, oldParallelism)
} }
if (parallelism < 1) {
parallelism = 1
}
val oldParallelism = database!!.parallelism
database?.parallelism = parallelism
actionInUIThreadAfterSaveDatabase = AfterParallelismSave(parallelism, oldParallelism)
} }
super.onDialogClosed(positiveResult) super.onDialogClosed(positiveResult)
@@ -63,16 +61,21 @@ class ParallelismPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDia
: ActionRunnable() { : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
val parallelismToShow = mNewParallelism val parallelismToShow =
if (!result.isSuccess) { if (result.isSuccess) {
database?.parallelism = mOldParallelism mNewParallelism
} } else {
database?.parallelism = mOldParallelism
mOldParallelism
}
preference.summary = parallelismToShow.toString() preference.summary = parallelismToShow.toString()
} }
} }
companion object { companion object {
const val MIN_PARALLELISM = 1
fun newInstance(key: String): ParallelismPreferenceDialogFragmentCompat { fun newInstance(key: String): ParallelismPreferenceDialogFragmentCompat {
val fragment = ParallelismPreferenceDialogFragmentCompat() val fragment = ParallelismPreferenceDialogFragmentCompat()
val bundle = Bundle(1) val bundle = Bundle(1)

View File

@@ -25,39 +25,38 @@ import android.widget.Toast
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
class RoundsPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() { class RoundsPreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) { override fun onBindDialogView(view: View) {
super.onBindDialogView(view) super.onBindDialogView(view)
explanationText = getString(R.string.rounds_explanation) explanationText = getString(R.string.rounds_explanation)
inputText = database?.numberKeyEncryptionRoundsAsString ?: "" inputText = database?.numberKeyEncryptionRounds?.toString() ?: MIN_ITERATIONS.toString()
} }
override fun onDialogClosed(positiveResult: Boolean) { override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult) { if (positiveResult) {
var rounds: Long database?.let { database ->
try { var rounds: Long = try {
val strRounds = inputText inputText.toLong()
rounds = java.lang.Long.parseLong(strRounds) } catch (e: NumberFormatException) {
} catch (e: NumberFormatException) { MIN_ITERATIONS
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() }
return if (rounds < MIN_ITERATIONS) {
} rounds = MIN_ITERATIONS
}
// TODO Max iterations
if (rounds < 1) { val oldRounds = database.numberKeyEncryptionRounds
rounds = 1 try {
} database.numberKeyEncryptionRounds = rounds
} catch (e: NumberFormatException) {
Toast.makeText(context, R.string.error_rounds_too_large, Toast.LENGTH_LONG).show()
database.numberKeyEncryptionRounds = Long.MAX_VALUE
}
val oldRounds = database!!.numberKeyEncryptionRounds actionInUIThreadAfterSaveDatabase = AfterRoundSave(rounds, oldRounds)
try {
database?.numberKeyEncryptionRounds = rounds
} catch (e: NumberFormatException) {
Toast.makeText(context, R.string.error_rounds_too_large, Toast.LENGTH_LONG).show()
database?.numberKeyEncryptionRounds = Integer.MAX_VALUE.toLong()
} }
actionInUIThreadAfterSaveDatabase = AfterRoundSave(rounds, oldRounds)
} }
super.onDialogClosed(positiveResult) super.onDialogClosed(positiveResult)
@@ -67,17 +66,21 @@ class RoundsPreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFr
private val mOldRounds: Long) : ActionRunnable() { private val mOldRounds: Long) : ActionRunnable() {
override fun onFinishRun(result: Result) { override fun onFinishRun(result: Result) {
val roundsToShow = mNewRounds val roundsToShow =
if (!result.isSuccess) { if (result.isSuccess) {
database?.numberKeyEncryptionRounds = mOldRounds mNewRounds
} } else {
database?.numberKeyEncryptionRounds = mOldRounds
mOldRounds
}
preference.summary = roundsToShow.toString() preference.summary = roundsToShow.toString()
} }
} }
companion object { companion object {
const val MIN_ITERATIONS = 1L
fun newInstance(key: String): RoundsPreferenceDialogFragmentCompat { fun newInstance(key: String): RoundsPreferenceDialogFragmentCompat {
val fragment = RoundsPreferenceDialogFragmentCompat() val fragment = RoundsPreferenceDialogFragmentCompat()
val bundle = Bundle(1) val bundle = Bundle(1)

View File

@@ -24,6 +24,7 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
/** /**
* Callback after a task is completed. * Callback after a task is completed.
@@ -52,8 +53,21 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable?
* launch the nested action runnable if exists and finish, * launch the nested action runnable if exists and finish,
* else directly finish * else directly finish
*/ */
protected fun finishRun(isSuccess: Boolean, message: String? = null) { protected fun finishRun(isSuccess: Boolean,
message: String? = null) {
finishRun(isSuccess, null, message)
}
/**
* If [success] or [executeNestedActionIfResultFalse] true,
* launch the nested action runnable if exists and finish,
* else directly finish
*/
protected fun finishRun(isSuccess: Boolean,
exception: LoadDatabaseException?,
message: String? = null) {
result.isSuccess = isSuccess result.isSuccess = isSuccess
result.exception = exception
result.message = message result.message = message
if (isSuccess || executeNestedActionIfResultFalse) { if (isSuccess || executeNestedActionIfResultFalse) {
execute() execute()
@@ -89,5 +103,8 @@ abstract class ActionRunnable(private var nestedActionRunnable: ActionRunnable?
/** /**
* Class to manage result from ActionRunnable * Class to manage result from ActionRunnable
*/ */
data class Result(var isSuccess: Boolean = true, var message: String? = null, var data: Bundle? = null) data class Result(var isSuccess: Boolean = true,
var message: String? = null,
var exception: LoadDatabaseException? = null,
var data: Bundle? = null)
} }

View File

@@ -60,8 +60,7 @@ class ClipboardHelper(private val context: Context) {
val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key), val sClipClear = prefs.getString(context.getString(R.string.clipboard_timeout_key),
context.getString(R.string.clipboard_timeout_default)) context.getString(R.string.clipboard_timeout_default))
val clipClearTime = java.lang.Long.parseLong(sClipClear ?: "60000") val clipClearTime = (sClipClear ?: "300000").toLong()
if (clipClearTime > 0) { if (clipClearTime > 0) {
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime) mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
} }

View File

@@ -20,7 +20,6 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import androidx.core.content.ContextCompat
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -29,9 +28,14 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.database.element.EntryVersioned
import com.kunzisoft.keepass.database.element.PwDate
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import java.text.DateFormat
import java.util.* import java.util.*
class EntryContentsView @JvmOverloads constructor(context: Context, class EntryContentsView @JvmOverloads constructor(context: Context,
@@ -59,9 +63,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val extrasContainerView: View private val extrasContainerView: View
private val extrasView: ViewGroup private val extrasView: ViewGroup
private val dateFormat: DateFormat = android.text.format.DateFormat.getDateFormat(context)
private val timeFormat: DateFormat = android.text.format.DateFormat.getTimeFormat(context)
private val creationDateView: TextView private val creationDateView: TextView
private val modificationDateView: TextView private val modificationDateView: TextView
private val lastAccessDateView: TextView private val lastAccessDateView: TextView
@@ -69,6 +70,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val uuidView: TextView private val uuidView: TextView
private val historyContainerView: View
private val historyListView: RecyclerView
private val historyAdapter = EntryHistoryAdapter(context)
val isUserNamePresent: Boolean val isUserNamePresent: Boolean
get() = userNameContainerView.visibility == View.VISIBLE get() = userNameContainerView.visibility == View.VISIBLE
@@ -103,6 +108,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidView = findViewById(R.id.entry_UUID) uuidView = findViewById(R.id.entry_UUID)
historyContainerView = findViewById(R.id.entry_history_container)
historyListView = findViewById(R.id.entry_history_list)
historyListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
adapter = historyAdapter
}
val attrColorAccent = intArrayOf(R.attr.colorAccent) val attrColorAccent = intArrayOf(R.attr.colorAccent)
val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent) val taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
colorAccent = taColorAccent.getColor(0, Color.BLACK) colorAccent = taColorAccent.getColor(0, Color.BLACK)
@@ -230,24 +242,20 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
extrasContainerView.visibility = View.GONE extrasContainerView.visibility = View.GONE
} }
private fun getDateTime(date: Date): String { fun assignCreationDate(date: PwDate) {
return dateFormat.format(date) + " " + timeFormat.format(date) creationDateView.text = date.getDateTimeString(resources)
} }
fun assignCreationDate(date: Date) { fun assignModificationDate(date: PwDate) {
creationDateView.text = getDateTime(date) modificationDateView.text = date.getDateTimeString(resources)
} }
fun assignModificationDate(date: Date) { fun assignLastAccessDate(date: PwDate) {
modificationDateView.text = getDateTime(date) lastAccessDateView.text = date.getDateTimeString(resources)
} }
fun assignLastAccessDate(date: Date) { fun assignExpiresDate(date: PwDate) {
lastAccessDateView.text = getDateTime(date) expiresDateView.text = date.getDateTimeString(resources)
}
fun assignExpiresDate(date: Date) {
expiresDateView.text = getDateTime(date)
} }
fun assignExpiresDate(constString: String) { fun assignExpiresDate(constString: String) {
@@ -258,6 +266,21 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidView.text = uuid.toString() uuidView.text = uuid.toString()
} }
fun showHistory(show: Boolean) {
historyContainerView.visibility = if (show) View.VISIBLE else View.GONE
}
fun assignHistory(history: ArrayList<EntryVersioned>) {
historyAdapter.clear()
historyAdapter.entryHistoryList.addAll(history)
}
fun onHistoryClick(action: (historyItem: EntryVersioned, position: Int)->Unit) {
historyAdapter.onItemClickListener = { item, position ->
action.invoke(item, position)
}
}
override fun generateDefaultLayoutParams(): LayoutParams { override fun generateDefaultLayoutParams(): LayoutParams {
return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
} }

View File

@@ -17,6 +17,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import java.util.HashMap
class EntryEditContentsView @JvmOverloads constructor(context: Context, class EntryEditContentsView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@@ -167,10 +168,12 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
/** /**
* Add a new view to fill in the information of the customized field * Add a new view to fill in the information of the customized field
*/ */
fun addNewCustomField(name: String = "", value:ProtectedString = ProtectedString(false, "")) { fun addNewCustomField(name: String = "", value: ProtectedString = ProtectedString(false, "")) {
val entryEditCustomField = EntryEditCustomField(context) val entryEditCustomField = EntryEditCustomField(context).apply {
entryEditCustomField.setData(name, value) setData(name, value)
entryEditCustomField.setFontVisibility(fontInVisibility) setFontVisibility(fontInVisibility)
requestFocus()
}
entryExtraFieldsContainer.addView(entryEditCustomField) entryExtraFieldsContainer.addView(entryEditCustomField)
} }

View File

@@ -19,13 +19,17 @@
*/ */
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import com.google.android.material.snackbar.Snackbar import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
/** /**
@@ -56,3 +60,40 @@ fun Activity.lockScreenOrientation() {
fun Activity.unlockScreenOrientation() { fun Activity.unlockScreenOrientation() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
private var actionBarHeight: Int = 0
fun Toolbar.collapse(animate: Boolean = true) {
if (layoutParams.height > 5)
actionBarHeight = layoutParams.height
val slideAnimator = ValueAnimator
.ofInt(height, 0)
if (animate)
slideAnimator.duration = 300L
slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int
requestLayout()
}
AnimatorSet().apply {
play(slideAnimator)
interpolator = AccelerateDecelerateInterpolator()
}.start()
}
fun Toolbar.expand(animate: Boolean = true) {
val slideAnimator = ValueAnimator
.ofInt(0, actionBarHeight)
if (animate)
slideAnimator.duration = 300L
slideAnimator.addUpdateListener { animation ->
layoutParams.height = animation.animatedValue as Int
requestLayout()
}
AnimatorSet().apply {
play(slideAnimator)
interpolator = AccelerateDecelerateInterpolator()
}.start()
}

View File

@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?attr/iconPreferenceColor"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?attr/iconPreferenceColor"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
</vector>

View File

@@ -75,7 +75,6 @@
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/entry_scroll" android:id="@+id/entry_scroll"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -86,12 +85,30 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/history_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="?attr/colorAccent"
android:padding="12dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textColor="?attr/textColorInverse"
android:text="@string/entry_history"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.kunzisoft.keepass.view.EntryContentsView <com.kunzisoft.keepass.view.EntryContentsView
android:id="@+id/entry_contents" android:id="@+id/entry_contents"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="0dp" android:layout_width="0dp"
app:layout_constraintWidth_percent="@dimen/content_percent" app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@+id/history_container"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -182,7 +182,7 @@
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/browse_button" android:id="@+id/open_database_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/select_database_file" android:text="@string/select_database_file"
@@ -194,72 +194,10 @@
android:paddingStart="32dp" android:paddingStart="32dp"
android:paddingRight="24dp" android:paddingRight="24dp"
android:paddingEnd="24dp" android:paddingEnd="24dp"
app:layout_constraintBottom_toTopOf="@+id/file_select_expandable"/> app:layout_constraintBottom_toTopOf="@+id/create_database_button"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/file_select_expandable_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:elevation="8dp"
app:layout_constraintTop_toTopOf="@+id/browse_button"
app:layout_constraintBottom_toBottomOf="@+id/browse_button"
app:layout_constraintStart_toStartOf="parent"
android:focusable="true"
android:padding="12dp"
android:tint="?attr/textColorInverse"
android:src="@drawable/ic_link_white_24dp"
android:contentDescription="@string/content_description_show_file_link"/>
<net.cachapa.expandablelayout.ExpandableLayout
android:id="@+id/file_select_expandable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/create_database"
app:el_duration="300"
app:el_expanded="false"
app:el_parallax="0.5">
<RelativeLayout
android:id="@+id/file_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorPrimaryDark">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/file_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:inputType="textUri"
android:textColor="?attr/textColorInverse"
android:textColorHint="?android:attr/textColorHintInverse"
android:layout_toLeftOf="@+id/open_database"
android:layout_toStartOf="@+id/open_database"
android:layout_marginLeft="@dimen/default_margin"
android:layout_marginStart="@dimen/default_margin"
android:layout_marginRight="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"
android:maxLines="1"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/open_database"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:layout_alignTop="@+id/file_filename"
android:layout_alignBottom="@+id/file_filename"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:focusable="true"
android:src="@drawable/ic_send_white_24dp"
android:contentDescription="@string/content_description_open_file_link"
android:tint="?attr/textColorInverse"/>
</RelativeLayout>
</net.cachapa.expandablelayout.ExpandableLayout>
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/create_database" android:id="@+id/create_database_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusable="true" android:focusable="true"

View File

@@ -27,7 +27,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_above="@+id/expandable_toolbar_paste_layout"> android:layout_above="@+id/toolbar_paste">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"
@@ -100,6 +100,7 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<LinearLayout <LinearLayout
android:id="@+id/node_list_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
@@ -112,6 +113,7 @@
android:padding="6dp" android:padding="6dp"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:background="?attr/colorAccent" android:background="?attr/colorAccent"
android:textColor="?attr/textColorInverse"
android:text="@string/selection_mode"/> android:text="@string/selection_mode"/>
<FrameLayout <FrameLayout
android:id="@+id/nodes_list_fragment_container" android:id="@+id/nodes_list_fragment_container"
@@ -124,28 +126,18 @@
android:id="@+id/add_node_button" android:id="@+id/add_node_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_anchor="@+id/nodes_list" app:layout_anchor="@+id/node_list_container"
app:layout_anchorGravity="right|bottom" /> app:layout_anchorGravity="right|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<net.cachapa.expandablelayout.ExpandableLayout <androidx.appcompat.widget.Toolbar
android:id="@+id/expandable_toolbar_paste_layout" android:id="@+id/toolbar_paste"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?attr/actionBarSize"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
app:el_duration="300" android:elevation="4dp"
app:el_expanded="false" android:theme="?attr/toolbarBottomAppearance"
app:el_parallax="0.5"> android:background="?attr/colorAccent"
tools:targetApi="lollipop" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_paste"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:theme="?attr/toolbarBottomAppearance"
android:background="?attr/colorAccent"
tools:targetApi="lollipop" />
</net.cachapa.expandablelayout.ExpandableLayout>
</RelativeLayout> </RelativeLayout>

View File

@@ -186,8 +186,8 @@
android:importantForAutofill="no" android:importantForAutofill="no"
android:layout_toEndOf="@+id/keyfile_checkox" android:layout_toEndOf="@+id/keyfile_checkox"
android:layout_toRightOf="@+id/keyfile_checkox" android:layout_toRightOf="@+id/keyfile_checkox"
android:layout_toLeftOf="@+id/browse_button" android:layout_toLeftOf="@+id/open_database_button"
android:layout_toStartOf="@+id/browse_button"> android:layout_toStartOf="@+id/open_database_button">
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/pass_keyfile" android:id="@+id/pass_keyfile"
@@ -203,7 +203,7 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/browse_button" android:id="@+id/open_database_button"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:padding="12dp" android:padding="12dp"

View File

@@ -114,7 +114,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/browse_button" android:id="@+id/open_database_button"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
@@ -130,8 +130,8 @@
android:id="@+id/keyfile_input_layout" android:id="@+id/keyfile_input_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/browse_button" android:layout_toLeftOf="@+id/open_database_button"
android:layout_toStartOf="@+id/browse_button" android:layout_toStartOf="@+id/open_database_button"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:importantForAutofill="no" android:importantForAutofill="no"
app:passwordToggleEnabled="true" app:passwordToggleEnabled="true"

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