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)
* New, more secure database creation workflow
* Recognize more database files

View File

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

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.os.Handler
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.UriUtil
import com.kunzisoft.keepass.view.EntryContentsView
import java.util.*
class EntryActivity : LockingHideActivity() {
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var entryContentsView: EntryContentsView? = null
private var toolbar: Toolbar? = null
private var mEntry: EntryVersioned? = null
private var mIsHistory: Boolean = false
private var mShowPassword: Boolean = false
private var clipboardHelper: ClipboardHelper? = null
@@ -82,12 +87,18 @@ class EntryActivity : LockingHideActivity() {
// Get Entry from UUID
try {
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
mEntry = currentDatabase.getEntryById(keyEntry)
} catch (e: ClassCastException) {
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) {
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
finish()
@@ -108,6 +119,7 @@ class EntryActivity : LockingHideActivity() {
// Get views
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
@@ -238,18 +250,11 @@ class EntryActivity : LockingHideActivity() {
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
// Assign dates
entry.creationTime.date?.let {
entryContentsView?.assignCreationDate(it)
}
entry.lastModificationTime.date?.let {
entryContentsView?.assignModificationDate(it)
}
entry.lastAccessTime.date?.let {
entryContentsView?.assignLastAccessDate(it)
}
val expires = entry.expiryTime.date
if (entry.isExpires && expires != null) {
entryContentsView?.assignExpiresDate(expires)
entryContentsView?.assignCreationDate(entry.creationTime)
entryContentsView?.assignModificationDate(entry.lastModificationTime)
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
if (entry.isExpires) {
entryContentsView?.assignExpiresDate(entry.expiryTime)
} else {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
@@ -257,6 +262,24 @@ class EntryActivity : LockingHideActivity() {
// Assign special data
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)
}
@@ -412,13 +435,16 @@ class EntryActivity : LockingHideActivity() {
companion object {
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)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, pw.nodeId)
intent.putExtra(KEY_ENTRY, entry.nodeId)
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
if (historyPosition != null)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
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.utils.MenuUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import java.util.*
class EntryEditActivity : LockingHideActivity(),
IconPickerDialogFragment.IconPickerListener,
@@ -90,7 +91,7 @@ class EntryEditActivity : LockingHideActivity(),
mDatabase = Database.getInstance()
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
mIsNew = false
// Create an Entry copy to modify from the database entry
mEntry = mDatabase?.getEntryById(it)
@@ -105,14 +106,12 @@ class EntryEditActivity : LockingHideActivity(),
}
}
// Retrieve the icon after an orientation change
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
} else {
// Create the new entry from the current one
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mEntry?.let { entry ->
// Create a copy to modify
mNewEntry = EntryVersioned(entry).also { newEntry ->
// WARNING Remove the parent to keep memory with parcelable
newEntry.parent = null
}
@@ -123,7 +122,11 @@ class EntryEditActivity : LockingHideActivity(),
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
mIsNew = true
// Create an empty new entry
if (savedInstanceState == null
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
mNewEntry = mDatabase?.createEntry()
}
mParent = mDatabase?.getGroupById(it)
// Add the default icon
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
if (mNewEntry == null || mParent == null) {
finish()
@@ -168,15 +177,17 @@ class EntryEditActivity : LockingHideActivity(),
// Set info in view
entryEditContentsView?.apply {
title = newEntry.title
username = newEntry.username
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
url = newEntry.url
password = newEntry.password
notes = newEntry.notes
for (entry in newEntry.customFields.entries) {
post {
addNewCustomField(entry.key, entry.value)
}
}
}
}
private fun populateEntryWithViews(newEntry: EntryVersioned) {
@@ -185,6 +196,7 @@ class EntryEditActivity : LockingHideActivity(),
newEntry.apply {
// Build info from view
entryEditContentsView?.let { entryView ->
removeAllFields()
title = entryView.title
username = entryView.username
url = entryView.url
@@ -218,8 +230,6 @@ class EntryEditActivity : LockingHideActivity(),
*/
private fun addNewCustomField() {
entryEditContentsView?.addNewCustomField()
// Scroll bottom
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
}
/**
@@ -342,7 +352,10 @@ class EntryEditActivity : LockingHideActivity(),
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
mNewEntry?.let {
populateEntryWithViews(it)
outState.putParcelable(KEY_NEW_ENTRY, it)
}
super.onSaveInstanceState(outState)
}

View File

@@ -39,7 +39,6 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import com.kunzisoft.keepass.R
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.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_file_selection.*
import net.cachapa.expandablelayout.ExpandableLayout
import java.io.FileNotFoundException
class FileDatabaseSelectActivity : StylishActivity(),
@@ -69,11 +66,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
// Views
private var fileListContainer: View? = null
private var createButtonView: View? = null
private var browseButtonView: View? = null
private var openButtonView: View? = null
private var fileSelectExpandableButtonView: View? = null
private var fileSelectExpandableLayout: ExpandableLayout? = null
private var openFileNameView: EditText? = null
private var openDatabaseButtonView: View? = null
// Adapter to manage database history list
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
@@ -84,8 +77,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
private var mOpenFileHelper: OpenFileHelper? = null
private var mDefaultPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -98,44 +89,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
toolbar.title = ""
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
createButtonView = findViewById(R.id.create_database)
createButtonView = findViewById(R.id.create_database_button)
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/x-keepass"
@@ -151,10 +106,8 @@ class FileDatabaseSelectActivity : StylishActivity(),
createButtonView?.setOnClickListener { createNewFile() }
mOpenFileHelper = OpenFileHelper(this)
browseButtonView = findViewById(R.id.browse_button)
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener {
UriUtil.parse(openFileNameView?.text?.toString())
})
openDatabaseButtonView = findViewById(R.id.open_database_button)
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
// History list
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
@@ -358,7 +311,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
private val keyFileUri: Uri?) : ActionRunnable() {
override fun run() {
finishRun(true, null)
finishRun(true)
}
override fun onFinishRun(result: Result) {
@@ -392,12 +345,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
if (uri != null) {
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
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) {
mDatabaseFileUri = data?.data
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
}
// else {
// TODO Show error
@@ -438,20 +387,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
})
if (!createDatabaseEducationPerformed) {
// selectDatabaseEducationPerformed
browseButtonView != null
openDatabaseButtonView != null
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
browseButtonView!!,
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
}
},
{
fileSelectExpandableButtonView?.let {
fileDatabaseSelectActivityEducation
.checkAndPerformedOpenLinkDatabaseEducation(it)
}
}
{}
)
}
}

View File

@@ -18,7 +18,6 @@
*/
package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.SearchManager
import android.app.assist.AssistStructure
@@ -29,16 +28,16 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
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.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
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.activities.dialogs.GroupEditDialogFragment
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.utils.MenuUtil
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(),
GroupEditDialogFragment.EditGroupListener,
@@ -75,7 +75,6 @@ class GroupActivity : LockingActivity(),
// Views
private var toolbar: Toolbar? = null
private var searchTitleView: View? = null
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
private var toolbarPaste: Toolbar? = null
private var iconView: ImageView? = null
private var modeTitleView: TextView? = null
@@ -115,10 +114,21 @@ class GroupActivity : LockingActivity(),
toolbar = findViewById(R.id.toolbar)
searchTitleView = findViewById(R.id.search_title)
groupNameView = findViewById(R.id.group_name)
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
toolbarPaste = findViewById(R.id.toolbar_paste)
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
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
@@ -129,9 +139,11 @@ class GroupActivity : LockingActivity(),
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
toolbarPaste?.expand(false)
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
toolbarPaste?.expand(false)
}
}
@@ -153,17 +165,6 @@ class GroupActivity : LockingActivity(),
// Update last access time.
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
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
@@ -449,7 +450,7 @@ class GroupActivity : LockingActivity(),
}
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
toolbarPaste?.expand()
mNodeToCopy = node
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
return false
@@ -457,7 +458,7 @@ class GroupActivity : LockingActivity(),
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse()
toolbarPaste?.collapse()
when (item.itemId) {
R.id.menu_paste -> {
@@ -489,7 +490,7 @@ class GroupActivity : LockingActivity(),
}
override fun onMoveMenuClick(node: NodeVersioned): Boolean {
toolbarPasteExpandableLayout?.expand()
toolbarPaste?.expand()
mNodeToMove = node
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
return false
@@ -497,7 +498,7 @@ class GroupActivity : LockingActivity(),
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
toolbarPasteExpandableLayout?.collapse()
toolbarPaste?.collapse()
when (item.itemId) {
R.id.menu_paste -> {

View File

@@ -44,6 +44,7 @@ import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.widget.*
import androidx.biometric.BiometricManager
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.PasswordEncodingDialogFragment
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.education.PasswordActivityEducation
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.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.MenuUtil
@@ -69,7 +72,6 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.asError
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
import java.lang.ref.WeakReference
class PasswordActivity : StylishActivity() {
@@ -87,6 +89,8 @@ class PasswordActivity : StylishActivity() {
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
private var prefs: SharedPreferences? = null
private var mRememberKeyFile: Boolean = false
@@ -101,8 +105,7 @@ class PasswordActivity : StylishActivity() {
prefs = PreferenceManager.getDefaultSharedPreferences(this)
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
resources.getBoolean(R.bool.keyfile_default))
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
setContentView(R.layout.activity_password)
@@ -123,7 +126,7 @@ class PasswordActivity : StylishActivity() {
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)
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
@@ -222,6 +225,7 @@ class PasswordActivity : StylishActivity() {
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
mDatabaseFileUri = databaseFileUri
mDatabaseKeyFileUri = keyFileUri
// Define title
databaseFileUri?.let {
@@ -243,11 +247,13 @@ class PasswordActivity : StylishActivity() {
newDefaultFileName = databaseFileUri ?: newDefaultFileName
}
newDefaultFileName?.let {
prefs?.edit()?.apply {
newDefaultFileName?.let {
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
apply()
} ?: kotlin.run {
remove(KEY_DEFAULT_DATABASE_PATH)
}
apply()
}
val backupManager = BackupManager(this@PasswordActivity)
@@ -391,14 +397,18 @@ class PasswordActivity : StylishActivity() {
keyFile: Uri?,
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity)
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
}
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
loadDatabase(password, keyFileUri)
verifyKeyFileCheckbox(keyFile)
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
}
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
}
private fun removePassword() {
@@ -406,7 +416,10 @@ class PasswordActivity : StylishActivity() {
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 {
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
@@ -418,30 +431,9 @@ class PasswordActivity : StylishActivity() {
val database = Database.getInstance()
database.closeAndClear(applicationContext.filesDir)
mDatabaseFileUri?.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()
}
}
/**
* Called after verify and try to opening the database
*/
private inner class AfterLoadingDatabase(val database: Database, val password: String?,
val cipherDatabaseEntity: CipherDatabaseEntity? = null)
: ActionRunnable() {
databaseFileUri?.let { databaseUri ->
val onFinishLoadDatabase = object: ActionRunnable() {
override fun onFinishRun(result: Result) {
runOnUiThread {
// Recheck fingerprint if error
@@ -453,6 +445,13 @@ class PasswordActivity : StylishActivity() {
}
if (result.isSuccess) {
// Save keyFile in app database
if (mRememberKeyFile) {
mDatabaseFileUri?.let { databaseUri ->
saveKeyFileData(databaseUri, mDatabaseKeyFileUri)
}
}
// Remove the password in view in all cases
removePassword()
@@ -460,23 +459,91 @@ class PasswordActivity : StylishActivity() {
if (cipherDatabaseEntity != null) {
CipherDatabaseAction.getInstance(this@PasswordActivity)
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
checkAndLaunchGroupActivity(database, password)
checkAndLaunchGroupActivity(database, password, keyFileUri)
}
} else {
checkAndLaunchGroupActivity(database, password)
checkAndLaunchGroupActivity(database, password, keyFileUri)
}
} else {
if (result.message != null && result.message!!.isNotEmpty()) {
Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show()
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()
}
}
}
}
private fun checkAndLaunchGroupActivity(database: Database, password: String?) {
if (database.validatePasswordEncoding(password)) {
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(database,
databaseUri,
password,
keyFileUri,
false,
onFinishLoadDatabase)
}
}
private fun showProgressDialogAndLoadDatabase(database: Database,
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()
} else {
PasswordEncodingDialogFragment().apply {

View File

@@ -45,6 +45,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var rootView: View? = null
private var passwordCheckBox: CompoundButton? = null
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
@@ -96,6 +98,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
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 inflater = activity.layoutInflater
@@ -104,9 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
.setTitle(R.string.assign_master_key)
// Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
@@ -116,7 +126,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
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) }
val dialog = builder.create()
@@ -132,7 +142,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
var error = verifyPassword() || verifyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(
@@ -193,6 +207,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
showEmptyPasswordConfirmationDialog()
}
}
return error
}
@@ -223,7 +238,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
this@AssignMasterKeyDialogFragment.dismiss()
}
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
@@ -238,7 +253,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox!!.isChecked, mKeyFile)
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
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
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
builder.setView(root)
.setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val textDescription = root.findViewById<TextView>(R.id.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()
}
.setNegativeButton(R.string.cancel) { _, _ ->
.setNegativeButton(android.R.string.cancel) { _, _ ->
val bundle = Bundle()
mListener?.cancelPassword(bundle)

View File

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

View File

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

View File

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

View File

@@ -83,7 +83,7 @@ class SortDialogFragment : DialogFragment() {
// Add action buttons
.setPositiveButton(android.R.string.ok
) { _, _ -> 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)
// Check if is ascending or descending

View File

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

View File

@@ -32,12 +32,10 @@ class AesKdf internal constructor() : KdfEngine() {
override val defaultParameters: KdfParameters
get() {
val p = KdfParameters(uuid)
p.setParamUUID()
p.setUInt32(ParamRounds, DEFAULT_ROUNDS.toLong())
return p
return KdfParameters(uuid).apply {
setParamUUID()
setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
}
}
override val defaultKeyRounds: Long
@@ -54,8 +52,8 @@ class AesKdf internal constructor() : KdfEngine() {
@Throws(IOException::class)
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
var currentMasterKey = masterKey
val rounds = p.getUInt64(ParamRounds)
var seed = p.getByteArray(ParamSeed)
val rounds = p.getUInt64(PARAM_ROUNDS)
var seed = p.getByteArray(PARAM_SEED)
if (currentMasterKey.size != 32) {
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
@@ -75,15 +73,15 @@ class AesKdf internal constructor() : KdfEngine() {
val seed = ByteArray(32)
random.nextBytes(seed)
p.setByteArray(ParamSeed, seed)
p.setByteArray(PARAM_SEED, seed)
}
override fun getKeyRounds(p: KdfParameters): Long {
return p.getUInt64(ParamRounds)
return p.getUInt64(PARAM_ROUNDS)
}
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
p.setUInt64(ParamRounds, keyRounds)
p.setUInt64(PARAM_ROUNDS, keyRounds)
}
companion object {
@@ -91,9 +89,24 @@ class AesKdf internal constructor() : KdfEngine() {
private const val DEFAULT_ROUNDS = 6000
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 ParamSeed = "S"
const val PARAM_ROUNDS = "R"
const val PARAM_SEED = "S"
}
}

View File

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

View File

@@ -30,17 +30,31 @@ abstract class KdfEngine : ObjectNameResource {
abstract val defaultParameters: KdfParameters
abstract val defaultKeyRounds: Long
@Throws(IOException::class)
abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray
abstract fun randomize(p: KdfParameters)
/*
* ITERATIONS
*/
abstract fun getKeyRounds(p: KdfParameters): 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 {
return UNKNOWN_VALUE.toLong()
}
@@ -49,9 +63,18 @@ abstract class KdfEngine : ObjectNameResource {
// Do nothing by default
}
open fun getDefaultMemoryUsage(): Long {
return UNKNOWN_VALUE.toLong()
}
open val defaultMemoryUsage: Long
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 {
return UNKNOWN_VALUE
@@ -61,13 +84,16 @@ abstract class KdfEngine : ObjectNameResource {
// Do nothing by default
}
open fun getDefaultParallelism(): Int {
return UNKNOWN_VALUE
}
open val defaultParallelism: Int
get() = UNKNOWN_VALUE
open val minParallelism: Int
get() = 1
open val maxParallelism: Int
get() = Int.MAX_VALUE
companion object {
const val UNKNOWN_VALUE = -1
const val UNKNOWN_VALUE_STRING = (-1).toString()
}
}

View File

@@ -19,37 +19,7 @@
*/
package com.kunzisoft.keepass.crypto.keyDerivation
import com.kunzisoft.keepass.database.exception.UnknownKDF
import java.util.ArrayList
object KdfFactory {
var aesKdf = AesKdf()
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 {
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 {
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 {
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.net.Uri
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.utils.UriUtil
import java.io.IOException
@@ -63,7 +63,7 @@ open class AssignPasswordInDatabaseRunnable @JvmOverloads constructor(
// To save the database
super.run()
finishRun(true)
} catch (e: InvalidKeyFileException) {
} catch (e: LoadDatabaseInvalidKeyFileException) {
erase(mBackupKey)
finishRun(false, e.message)
} catch (e: IOException) {

View File

@@ -19,118 +19,45 @@
*/
package com.kunzisoft.keepass.database.action
import android.content.Context
import android.content.ContentResolver
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.exception.*
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.search.SearchDbHelper
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.FileNotFoundException
import java.io.IOException
import java.lang.ref.WeakReference
import java.io.File
class LoadDatabaseRunnable(private val mWeakContext: WeakReference<Context>,
private val mDatabase: Database,
class LoadDatabaseRunnable(private val mDatabase: Database,
private val mUri: Uri,
private val mPass: String?,
private val mKey: Uri?,
private val contentResolver: ContentResolver,
private val cacheDirectory: File,
private val mSearchHelper: SearchDbHelper,
private val mFixDuplicateUUID: Boolean,
private val progressTaskUpdater: ProgressTaskUpdater?,
nestedAction: ActionRunnable)
: 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() {
try {
mWeakContext.get()?.let {
mDatabase.loadData(it, mUri, mPass, mKey, progressTaskUpdater)
saveFileData(mUri, mKey)
mDatabase.loadData(mUri, mPass, mKey,
contentResolver,
cacheDirectory,
mSearchHelper,
mFixDuplicateUUID,
progressTaskUpdater)
finishRun(true)
} ?: finishRun(false, "Context null")
} catch (e: ArcFourException) {
catchError(e, R.string.error_arc4)
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
}
}
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)
catch (e: LoadDatabaseException) {
finishRun(false, e)
}
}
override fun onFinishRun(result: Result) {
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 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 java.io.IOException
@@ -37,7 +37,7 @@ abstract class SaveDatabaseRunnable(protected var context: Context,
database.saveData(context.contentResolver)
} catch (e: IOException) {
finishRun(false, e.message)
} catch (e: PwDbOutputException) {
} catch (e: DatabaseOutputException) {
finishRun(false, e.message)
}
}

View File

@@ -74,6 +74,8 @@ class DeleteEntryRunnable constructor(
result.data = Bundle().apply {
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

View File

@@ -73,6 +73,8 @@ class DeleteGroupRunnable(context: FragmentActivity,
result.data = Bundle().apply {
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

View File

@@ -33,11 +33,17 @@ class UpdateEntryRunnable constructor(
: ActionNodeDatabaseRunnable(context, database, finishRunnable, save) {
// Keep backup of original values in case save fails
private var mBackupEntry: EntryVersioned? = null
private var mBackupEntryHistory: EntryVersioned? = null
override fun nodeAction() {
mBackupEntry = database.addHistoryBackupTo(mOldEntry)
mOldEntry.touch(modified = true, touchParents = true)
mNewEntry.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
mOldEntry.updateWith(mNewEntry)
}
@@ -45,7 +51,7 @@ class UpdateEntryRunnable constructor(
override fun nodeFinish(result: Result): ActionNodeValues {
if (!result.isSuccess) {
// If we fail to save, back out changes to global structure
mBackupEntry?.let {
mBackupEntryHistory?.let {
mOldEntry.updateWith(it)
}
}

View File

@@ -20,14 +20,12 @@
package com.kunzisoft.keepass.database.element
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.database.Cursor
import android.net.Uri
import android.util.Log
import android.webkit.URLUtil
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.cursor.EntryCursorV3
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.search.SearchDbHelper
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.stream.LEDataInputStream
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
@@ -57,7 +54,8 @@ class Database {
private var pwDatabaseV4: PwDatabaseV4? = null
private var mUri: Uri? = null
private var searchHelper: SearchDbHelper? = null
private var mSearchHelper: SearchDbHelper? = null
var isReadOnly = false
val drawFactory = IconDrawableFactory()
@@ -69,15 +67,23 @@ class Database {
return pwDatabaseV3?.iconFactory ?: pwDatabaseV4?.iconFactory ?: PwIconFactory()
}
val name: String
var name: String
get() {
return pwDatabaseV4?.name ?: ""
}
set(name) {
pwDatabaseV4?.name = name
pwDatabaseV4?.nameChanged = PwDate()
}
val description: String
var description: String
get() {
return pwDatabaseV4?.description ?: ""
}
set(description) {
pwDatabaseV4?.description = description
pwDatabaseV4?.descriptionChanged = PwDate()
}
var defaultUsername: String
get() {
@@ -88,32 +94,55 @@ class Database {
pwDatabaseV4?.defaultUserNameChanged = PwDate()
}
val encryptionAlgorithm: PwEncryptionAlgorithm?
// with format "#000000"
var color: String
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>
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>
get() {
if (pwDatabaseV3 != null) {
return KdfFactory.kdfListV3
}
if (pwDatabaseV4 != null) {
return KdfFactory.kdfListV4
}
return ArrayList()
}
get() = pwDatabaseV3?.kdfAvailableList ?: pwDatabaseV4?.kdfAvailableList ?: ArrayList()
val kdfEngine: KdfEngine
get() {
return pwDatabaseV4?.kdfEngine ?: return KdfFactory.aesKdf
var kdfEngine: KdfEngine?
get() = pwDatabaseV3?.kdfEngine ?: pwDatabaseV4?.kdfEngine
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
get() = pwDatabaseV3?.numberKeyEncryptionRounds ?: pwDatabaseV4?.numberKeyEncryptionRounds ?: 0
@@ -123,9 +152,6 @@ class Database {
pwDatabaseV4?.numberKeyEncryptionRounds = numberRounds
}
val memoryUsageAsString: String
get() = memoryUsage.toString()
var memoryUsage: Long
get() {
return pwDatabaseV4?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong()
@@ -134,9 +160,6 @@ class Database {
pwDatabaseV4?.memoryUsage = memory
}
val parallelismAsString: String
get() = parallelism.toString()
var parallelism: Int
get() = pwDatabaseV4?.parallelism ?: KdfEngine.UNKNOWN_VALUE
set(parallelism) {
@@ -161,6 +184,25 @@ class Database {
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
* @return true if RecycleBin available
@@ -206,8 +248,15 @@ class Database {
this.mUri = databaseUri
}
@Throws(IOException::class, InvalidDBException::class)
fun loadData(ctx: Context, uri: Uri, password: String?, keyfile: Uri?, progressTaskUpdater: ProgressTaskUpdater?) {
@Throws(LoadDatabaseException::class)
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
contentResolver: ContentResolver,
cacheDirectory: File,
searchHelper: SearchDbHelper,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
try {
mUri = uri
isReadOnly = false
@@ -219,20 +268,20 @@ class Database {
// Pass Uris as InputStreams
val inputStream: InputStream?
try {
inputStream = UriUtil.getUriInputStream(ctx.contentResolver, uri)
inputStream = UriUtil.getUriInputStream(contentResolver, uri)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw ContentFileNotFoundException.getInstance(uri)
throw LoadDatabaseFileNotFoundException()
}
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
keyfile?.let {
try {
keyFileInputStream = UriUtil.getUriInputStream(ctx.contentResolver, keyfile)
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
} catch (e: Exception) {
Log.e("KPD", "Database::loadData", e)
throw ContentFileNotFoundException.getInstance(keyfile)
throw LoadDatabaseFileNotFoundException()
}
}
@@ -262,22 +311,32 @@ class Database {
progressTaskUpdater))
// Header of database V4
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(ctx.filesDir)
PwDbHeaderV4.matchesHeader(sig1, sig2) -> setDatabaseV4(ImporterV4(
cacheDirectory,
fixDuplicateUUID)
.openDatabase(bufferedInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
// Header not recognized
else -> throw InvalidDBSignatureException()
else -> throw LoadDatabaseSignatureException()
}
try {
searchHelper = SearchDbHelper(PreferencesUtil.omitBackup(ctx))
this.mSearchHelper = searchHelper
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) {
Log.e(TAG, "Load can't be performed with this Database version", e)
loaded = false
throw LoadDatabaseException(e)
}
}
@@ -289,7 +348,7 @@ class Database {
@JvmOverloads
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? {
@@ -340,14 +399,14 @@ class Database {
return entry
}
@Throws(IOException::class, PwDbOutputException::class)
@Throws(IOException::class, DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver) {
mUri?.let {
saveData(contentResolver, it)
}
}
@Throws(IOException::class, PwDbOutputException::class)
@Throws(IOException::class, DatabaseOutputException::class)
private fun saveData(contentResolver: ContentResolver, uri: Uri) {
val errorMessage = "Failed to store database."
@@ -425,31 +484,30 @@ class Database {
return false
}
fun assignName(name: String) {
pwDatabaseV4?.name = name
pwDatabaseV4?.nameChanged = PwDate()
}
fun containsDescription(): Boolean {
pwDatabaseV4?.let { return true }
return false
}
fun assignDescription(description: String) {
pwDatabaseV4?.description = description
pwDatabaseV4?.descriptionChanged = PwDate()
fun containsDefaultUsername(): Boolean {
pwDatabaseV4?.let { return true }
return false
}
fun containsCustomColor(): Boolean {
pwDatabaseV4?.let { return true }
return false
}
fun allowNoMasterKey(): Boolean {
pwDatabaseV4?.let { return true }
return false
}
fun allowEncryptionAlgorithmModification(): Boolean {
return availableEncryptionAlgorithms.size > 1
}
fun assignEncryptionAlgorithm(algorithm: PwEncryptionAlgorithm) {
pwDatabaseV4?.encryptionAlgorithm = algorithm
pwDatabaseV4?.setDataEngine(algorithm.cipherEngine)
pwDatabaseV4?.dataCipher = algorithm.dataCipher
}
fun getEncryptionAlgorithmName(resources: Resources): String {
return pwDatabaseV3?.encryptionAlgorithm?.getName(resources) ?: pwDatabaseV4?.encryptionAlgorithm?.getName(resources) ?: ""
}
@@ -458,25 +516,17 @@ class Database {
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 {
return kdfEngine.getName(resources)
return kdfEngine?.getName(resources) ?: ""
}
fun validatePasswordEncoding(key: String?): Boolean {
return pwDatabaseV3?.validatePasswordEncoding(key)
?: pwDatabaseV4?.validatePasswordEncoding(key)
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
return pwDatabaseV3?.validatePasswordEncoding(password, containsKeyFile)
?: pwDatabaseV4?.validatePasswordEncoding(password, containsKeyFile)
?: false
}
@Throws(InvalidKeyFileException::class, IOException::class)
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
pwDatabaseV3?.retrieveMasterKey(key, keyInputStream)
pwDatabaseV4?.retrieveMasterKey(key, keyInputStream)
@@ -516,7 +566,7 @@ class Database {
return null
}
fun getEntryById(id: PwNodeId<*>): EntryVersioned? {
fun getEntryById(id: PwNodeId<UUID>): EntryVersioned? {
pwDatabaseV3?.getEntryById(id)?.let {
return EntryVersioned(it)
}
@@ -527,9 +577,11 @@ class Database {
}
fun getGroupById(id: PwNodeId<*>): GroupVersioned? {
if (id is PwNodeIdInt)
pwDatabaseV3?.getGroupById(id)?.let {
return GroupVersioned(it)
}
else if (id is PwNodeIdUUID)
pwDatabaseV4?.getGroupById(id)?.let {
return GroupVersioned(it)
}
@@ -702,31 +754,28 @@ class Database {
}
}
fun addHistoryBackupTo(entry: EntryVersioned): EntryVersioned {
val backupEntry = EntryVersioned(entry)
fun removeOldestHistory(entry: EntryVersioned) {
entry.addBackupToHistory()
// Remove oldest backup if more than max items or max memory
// Remove oldest history if more than max items or max memory
pwDatabaseV4?.let {
val history = entry.getHistory()
val maxItems = it.historyMaxItems
val maxItems = historyMaxItems
if (maxItems >= 0) {
while (history.size > maxItems) {
entry.removeOldestEntryFromHistory()
}
}
val maxSize = it.historyMaxSize
val maxSize = historyMaxSize
if (maxSize >= 0) {
while (true) {
var histSize: Long = 0
for (backup in history) {
histSize += backup.size
var historySize: Long = 0
for (entryHistory in history) {
historySize += entryHistory.getSize()
}
if (histSize > maxSize) {
if (historySize > maxSize) {
entry.removeOldestEntryFromHistory()
} else {
break
@@ -734,8 +783,6 @@ class Database {
}
}
}
return backupEntry
}
companion object : SingletonHolder<Database>(::Database) {

View File

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

View File

@@ -17,25 +17,24 @@
* 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
// since we won't do arithmetic on these values (also unlikely to
// reach negative ids).
enum class PwCompressionAlgorithm constructor(val id: Int) {
enum class PwCompressionAlgorithm : ObjectNameResource {
None(0),
Gzip(1);
None,
GZip;
companion object {
fun fromId(num: Int): PwCompressionAlgorithm? {
for (e in values()) {
if (e.id == num) {
return e
}
}
return null
override fun getName(resources: Resources): String {
return when (this) {
None -> resources.getString(R.string.compression_none)
GZip -> resources.getString(R.string.compression_gzip)
}
}

View File

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

View File

@@ -20,7 +20,9 @@
package com.kunzisoft.keepass.database.element
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 java.io.IOException
import java.io.InputStream
@@ -28,18 +30,25 @@ import java.security.DigestOutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* @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>() {
class PwDatabaseV3 : PwDatabase<Int, PwGroupV3, PwEntryV3>() {
private var numKeyEncRounds: Int = 0
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
override val version: String
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>
get() {
val list = ArrayList<PwEncryptionAlgorithm>()
@@ -103,7 +112,7 @@ class PwDatabaseV3 : PwDatabase<PwGroupV3, PwEntryV3>() {
return newId
}
@Throws(InvalidKeyFileException::class, IOException::class)
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
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.engine.AesEngine
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.exception.InvalidKeyFileException
import com.kunzisoft.keepass.crypto.keyDerivation.*
import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidKeyFileException
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.PwCompressionAlgorithm
import com.kunzisoft.keepass.utils.VariantDictionary
import org.w3c.dom.Node
import org.w3c.dom.Text
@@ -43,14 +40,15 @@ import java.util.*
import javax.xml.parsers.DocumentBuilderFactory
class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
class PwDatabaseV4 : PwDatabase<UUID, PwGroupV4, PwEntryV4> {
var hmacKey: ByteArray? = null
private set
var dataCipher = AesEngine.CIPHER_UUID
private var dataEngine: CipherEngine = AesEngine()
var compressionAlgorithm = PwCompressionAlgorithm.Gzip
var compressionAlgorithm = PwCompressionAlgorithm.GZip
var kdfParameters: KdfParameters? = null
private var kdfV4List: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary()
@@ -93,6 +91,11 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
var localizedAppName = "KeePassDX" // TODO resource
init {
kdfV4List.add(KdfFactory.aesKdf)
kdfV4List.add(KdfFactory.argon2Kdf)
}
constructor()
constructor(databaseName: String) {
@@ -107,6 +110,39 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
override val version: String
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>
get() {
val list = ArrayList<PwEncryptionAlgorithm>()
@@ -116,45 +152,45 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
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
get() {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null)
numKeyEncRounds = kdfEngine!!.getKeyRounds(kdfParameters!!)
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
return numKeyEncRounds
}
@Throws(NumberFormatException::class)
set(rounds) {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null)
kdfEngine!!.setKeyRounds(kdfParameters!!, rounds)
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
numKeyEncRounds = rounds
}
var memoryUsage: Long
get() = if (kdfEngine != null && kdfParameters != null) {
kdfEngine!!.getMemoryUsage(kdfParameters!!)
get() {
val kdfEngine = kdfEngine
return if (kdfEngine != null && kdfParameters != null) {
kdfEngine.getMemoryUsage(kdfParameters!!)
} else KdfEngine.UNKNOWN_VALUE.toLong()
}
set(memory) {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null)
kdfEngine!!.setMemoryUsage(kdfParameters!!, memory)
kdfEngine.setMemoryUsage(kdfParameters!!, memory)
}
var parallelism: Int
get() = if (kdfEngine != null && kdfParameters != null) {
kdfEngine!!.getParallelism(kdfParameters!!)
get() {
val kdfEngine = kdfEngine
return if (kdfEngine != null && kdfParameters != null) {
kdfEngine.getParallelism(kdfParameters!!)
} else KdfEngine.UNKNOWN_VALUE
}
set(parallelism) {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null)
kdfEngine!!.setParallelism(kdfParameters!!, parallelism)
kdfEngine.setParallelism(kdfParameters!!, parallelism)
}
override val passwordEncoding: String
@@ -200,7 +236,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
return getCustomData().isNotEmpty()
}
@Throws(InvalidKeyFileException::class, IOException::class)
@Throws(LoadDatabaseInvalidKeyFileException::class, IOException::class)
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
var masterKey = byteArrayOf()
@@ -227,7 +263,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
fun makeFinalKey(masterSeed: ByteArray) {
kdfParameters?.let { keyDerivationFunctionParameters ->
val kdfEngine = KdfFactory.getEngineV4(keyDerivationFunctionParameters)
val kdfEngine = getEngineV4(keyDerivationFunctionParameters)
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
if (transformedMasterKey.size != 32) {
@@ -360,9 +396,7 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
}
addGroupTo(recycleBinGroup, rootGroup)
recycleBinUUID = recycleBinGroup.id
recycleBinGroup.lastModificationTime.date?.let {
recycleBinChanged = it
}
recycleBinChanged = recycleBinGroup.lastModificationTime.date
}
}
@@ -427,10 +461,10 @@ class PwDatabaseV4 : PwDatabase<PwGroupV4, PwEntryV4> {
return publicCustomData.size() > 0
}
override fun validatePasswordEncoding(key: String?): Boolean {
if (key == null)
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (password == null)
return true
return super.validatePasswordEncoding(key)
return super.validatePasswordEncoding(password, containsKeyFile)
}
override fun clearCache() {

View File

@@ -19,14 +19,12 @@
*/
package com.kunzisoft.keepass.database.element
import android.content.res.Resources
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ConfigurationCompat
import com.kunzisoft.keepass.utils.Types
import java.util.Arrays
import java.util.Calendar
import java.util.Date
import java.util.*
/**
* Converting from the C Date format to the Java data format is
@@ -34,14 +32,14 @@ import java.util.Date
*/
class PwDate : Parcelable {
private var jDate: Date? = null
private var jDate: Date = Date()
private var jDateBuilt = false
@Transient
private var cDate: ByteArray? = null
@Transient
private var cDateBuilt = false
val date: Date?
val date: Date
get() {
if (!jDateBuilt) {
jDate = readTime(cDate, 0, calendar)
@@ -68,9 +66,7 @@ class PwDate : Parcelable {
}
constructor(source: PwDate) {
if (source.jDate != null) {
this.jDate = Date(source.jDate!!.time)
}
this.jDate = Date(source.jDate.time)
this.jDateBuilt = source.jDateBuilt
if (source.cDate != null) {
@@ -106,6 +102,10 @@ class PwDate : Parcelable {
return 0
}
fun getDateTimeString(resources: Resources): String {
return Companion.getDateTimeString(resources, this.date)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(date)
dest.writeByte((if (jDateBuilt) 1 else 0).toByte())
@@ -135,7 +135,7 @@ class PwDate : Parcelable {
}
override fun hashCode(): Int {
var result = jDate?.hashCode() ?: 0
var result = jDate.hashCode()
result = 31 * result + jDateBuilt.hashCode()
result = 31 * result + (cDate?.contentHashCode() ?: 0)
result = 31 * result + cDateBuilt.hashCode()
@@ -280,5 +280,13 @@ class PwDate : Parcelable {
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
* @param source
*/
fun updateWith(source: PwEntryV4) {
fun updateWith(source: PwEntryV4, copyHistory: Boolean = true) {
super.updateWith(source)
iconCustom = PwIconCustom(source.iconCustom)
usageCount = source.usageCount
@@ -146,6 +146,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
overrideURL = source.overrideURL
autoType = AutoType(source.autoType)
history.clear()
if (copyHistory)
history.addAll(source.history)
url = source.url
additional = source.additional
@@ -263,6 +264,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
return true
}
fun removeAllFields() {
fields.clear()
}
fun addExtraField(label: String, value: ProtectedString) {
fields[label] = value
}
@@ -287,6 +292,10 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
history.add(entry)
}
fun removeAllHistory() {
history.clear()
}
fun removeOldestEntryFromHistory() {
var min: Date? = null
var index = -1
@@ -294,7 +303,7 @@ class PwEntryV4 : PwEntry<PwGroupV4, PwEntryV4>, PwNodeV4Interface {
for (i in history.indices) {
val entry = history[i]
val lastMod = entry.lastModificationTime.date
if (min == null || lastMod == null || lastMod.before(min)) {
if (min == null || lastMod.before(min)) {
index = i
min = lastMod
}

View File

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

View File

@@ -90,7 +90,7 @@ abstract class PwNode<IdType, Parent : PwGroupInterface<Parent, Entry>, Entry :
final override var isExpires: Boolean
// If expireDate is before NEVER_EXPIRE date less 1 month (to be sure)
get() = expiryTime.date
?.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate()) ?: true
.before(LocalDate.fromDateFields(PwDate.NEVER_EXPIRE).minusMonths(1).toDate())
set(value) {
if (!value) {
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
class PwDbOutputException : Exception {
class DatabaseOutputException : Exception {
constructor(string: String) : super(string)
constructor(string: String, e: Exception) : super(string, 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
class SamsungClipboardException(e: Exception) : Exception(e) {
companion object {
private const val serialVersionUID = -3168837280393843509L
}
}
class SamsungClipboardException(e: Exception) : Exception(e)

View File

@@ -2,8 +2,4 @@ package com.kunzisoft.keepass.database.exception
import java.io.IOException
class UnknownKDF : IOException(message) {
companion object {
private const val message = "Unknown key derivation function"
}
}
class UnknownKDF : IOException("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.KdfParameters
import com.kunzisoft.keepass.database.NodeHandler
import com.kunzisoft.keepass.database.element.PwNodeV4Interface
import com.kunzisoft.keepass.database.element.PwDatabaseV4
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.database.element.*
import com.kunzisoft.keepass.database.exception.LoadDatabaseVersionException
import com.kunzisoft.keepass.stream.CopyInputStream
import com.kunzisoft.keepass.stream.HmacBlockStream
import com.kunzisoft.keepass.stream.LEDataInputStream
@@ -51,10 +48,10 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
// version < FILE_VERSION_32_4)
var transformSeed: ByteArray?
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.ParamSeed)
get() = databaseV4.kdfParameters?.getByteArray(AesKdf.PARAM_SEED)
private set(seed) {
assignAesKdfEngineIfNotExists()
databaseV4.kdfParameters?.setByteArray(AesKdf.ParamSeed, seed)
databaseV4.kdfParameters?.setByteArray(AesKdf.PARAM_SEED, seed)
}
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
* @param inputStream
* @throws IOException
* @throws InvalidDBVersionException
* @throws LoadDatabaseVersionException
*/
@Throws(IOException::class, InvalidDBVersionException::class)
@Throws(IOException::class, LoadDatabaseVersionException::class)
fun loadFromFile(inputStream: InputStream): HeaderAndHash {
val messageDigest: MessageDigest
try {
@@ -153,12 +150,12 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
val sig2 = littleEndianDataInputStream.readInt()
if (!matchesHeader(sig1, sig2)) {
throw InvalidDBVersionException()
throw LoadDatabaseVersionException()
}
version = littleEndianDataInputStream.readUInt() // Erase previous value
if (!validVersion(version)) {
throw InvalidDBVersionException()
throw LoadDatabaseVersionException()
}
var done = false
@@ -229,7 +226,9 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
}
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
}
}
@@ -246,7 +245,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
private fun setTransformRound(roundsByte: ByteArray?) {
assignAesKdfEngineIfNotExists()
val rounds = LEDataInputStream.readLong(roundsByte!!, 0)
databaseV4.kdfParameters?.setUInt64(AesKdf.ParamRounds, rounds)
databaseV4.kdfParameters?.setUInt64(AesKdf.PARAM_ROUNDS, rounds)
databaseV4.numberKeyEncryptionRounds = rounds
}
@@ -261,7 +260,7 @@ class PwDbHeaderV4(private val databaseV4: PwDatabaseV4) : PwDbHeader() {
throw IOException("Unrecognized compression flag.")
}
PwCompressionAlgorithm.fromId(flag)?.let { compression ->
getCompressionFromFlag(flag)?.let { 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_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 {
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
}

View File

@@ -20,13 +20,13 @@
package com.kunzisoft.keepass.database.file.load
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 java.io.IOException
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.
@@ -36,9 +36,9 @@ abstract class Importer<PwDb : PwDatabase<*, *>> {
* @return new PwDatabase container.
*
* @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,
password: String?,
keyInputStream: InputStream?,

View File

@@ -73,7 +73,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
private lateinit var mDatabaseToOpen: PwDatabaseV3
@Throws(IOException::class, InvalidDBException::class)
@Throws(IOException::class, LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
@@ -92,11 +92,11 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
hdr.loadFromFile(filebuf, 0)
if (hdr.signature1 != PwDbHeader.PWM_DBSIG_1 || hdr.signature2 != PwDbHeaderV3.DBSIG_2) {
throw InvalidDBSignatureException()
throw LoadDatabaseSignatureException()
}
if (!hdr.matchesVersion()) {
throw InvalidDBVersionException()
throw LoadDatabaseVersionException()
}
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
@@ -109,7 +109,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
} else if (hdr.flags and PwDbHeaderV3.FLAG_TWOFISH != 0) {
mDatabaseToOpen.encryptionAlgorithm = PwEncryptionAlgorithm.Twofish
} else {
throw InvalidAlgorithmException()
throw LoadDatabaseInvalidAlgorithmException()
}
mDatabaseToOpen.numberKeyEncryptionRounds = hdr.numKeyEncRounds.toLong()
@@ -152,7 +152,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
} catch (e1: IllegalBlockSizeException) {
throw IOException("Invalid block size")
} catch (e1: BadPaddingException) {
throw InvalidPasswordException()
throw LoadDatabaseInvalidPasswordException()
}
val md: MessageDigest
@@ -171,7 +171,7 @@ class ImporterV3 : Importer<PwDatabaseV3>() {
if (!Arrays.equals(hash, hdr.contentsHash)) {
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())

View File

@@ -24,11 +24,11 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory
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.exception.ArcFourException
import com.kunzisoft.keepass.database.exception.InvalidDBException
import com.kunzisoft.keepass.database.exception.InvalidPasswordException
import com.kunzisoft.keepass.database.exception.LoadDatabaseArcFourException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseInvalidPasswordException
import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -56,7 +56,8 @@ import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
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 lateinit var mDatabase: PwDatabaseV4
@@ -89,7 +90,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
private var entryCustomDataKey: String? = null
private var entryCustomDataValue: String? = null
@Throws(IOException::class, InvalidDBException::class)
@Throws(IOException::class, LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
@@ -99,6 +100,9 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabase = PwDatabaseV4()
mDatabase.changeDuplicateId = fixDuplicateUUID
val header = PwDbHeaderV4(mDatabase)
val headerAndHash = header.loadFromFile(databaseInputStream)
@@ -138,14 +142,14 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
try {
storedStartBytes = dataDecrypted.readBytes(32)
if (storedStartBytes == null || storedStartBytes.size != 32) {
throw InvalidPasswordException()
throw LoadDatabaseInvalidPasswordException()
}
} catch (e: IOException) {
throw InvalidPasswordException()
throw LoadDatabaseInvalidPasswordException()
}
if (!Arrays.equals(storedStartBytes, header.streamStartBytes)) {
throw InvalidPasswordException()
throw LoadDatabaseInvalidPasswordException()
}
isPlain = HashedBlockInputStream(dataDecrypted)
@@ -153,18 +157,18 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
val isData = LEDataInputStream(databaseInputStream)
val storedHash = isData.readBytes(32)
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 storedHmac = isData.readBytes(32)
if (storedHmac == null || storedHmac.size != 32) {
throw InvalidDBException()
throw LoadDatabaseException()
}
// Mac doesn't match
if (!Arrays.equals(headerHmac, storedHmac)) {
throw InvalidDBException()
throw LoadDatabaseException()
}
val hmIs = HmacBlockInputStream(isData, true, hmacKey)
@@ -173,10 +177,9 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
}
val isXml: InputStream
if (mDatabase.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
isXml = GZIPInputStream(isPlain)
} else {
isXml = isPlain
isXml = when(mDatabase.compressionAlgorithm) {
PwCompressionAlgorithm.GZip -> GZIPInputStream(isPlain)
else -> isPlain
}
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)
if (randomStream == null) {
throw ArcFourException()
throw LoadDatabaseArcFourException()
}
readXmlStreamed(isXml)
@@ -271,7 +274,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
Binaries
}
@Throws(IOException::class, InvalidDBException::class)
@Throws(IOException::class, LoadDatabaseException::class)
private fun readXmlStreamed(readerStream: InputStream) {
try {
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) {
ctxGroups.clear()
@@ -313,7 +316,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
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 {
val name = xpp.name
when (ctx) {
@@ -337,7 +340,7 @@ class ImporterV4(private val streamDir: File) : Importer<PwDatabaseV4>() {
if (encodedHash.isNotEmpty() && hashOfHeader != null) {
val hash = Base64Coder.decode(encodedHash)
if (!Arrays.equals(hash, hashOfHeader)) {
throw InvalidDBException()
throw LoadDatabaseException()
}
}
} 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)) {
mDatabase.defaultUserNameChanged = readPwTime(xpp)
} 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)
} else if (name.equals(PwDatabaseV4XML.ElemDbMntncHistoryDays, ignoreCase = true)) {
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.file.PwDbHeader
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.LEDataOutputStream
import com.kunzisoft.keepass.stream.MacOutputStream
@@ -41,7 +41,7 @@ import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
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() {
private val los: LEDataOutputStream
private val mos: MacOutputStream
@@ -54,13 +54,13 @@ constructor(private val db: PwDatabaseV4, private val header: PwDbHeaderV4, os:
try {
md = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("SHA-256 not implemented here.")
throw DatabaseOutputException("SHA-256 not implemented here.")
}
try {
db.makeFinalKey(header.masterSeed)
} catch (e: IOException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
}
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")
hmac.init(signingKey)
} catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
} catch (e: InvalidKeyException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
}
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(header.version)
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)
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {

View File

@@ -20,7 +20,7 @@
package com.kunzisoft.keepass.database.file.save
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.security.NoSuchAlgorithmException
@@ -28,13 +28,13 @@ import java.security.SecureRandom
abstract class PwDbOutput<Header : PwDbHeader> protected constructor(protected var mOS: OutputStream) {
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom {
val random: SecureRandom
try {
random = SecureRandom.getInstance("SHA1PRNG")
} 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)
@@ -43,10 +43,10 @@ abstract class PwDbOutput<Header : PwDbHeader> protected constructor(protected v
return random
}
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
abstract fun output()
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
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.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.PwDbHeaderV3
import com.kunzisoft.keepass.stream.LEDataOutputStream
@@ -42,18 +42,18 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
private var headerHashBlock: ByteArray? = null
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
fun getFinalKey(header: PwDbHeader): ByteArray? {
try {
val h3 = header as PwDbHeaderV3
mDatabaseV3.makeFinalKey(h3.masterSeed, h3.transformSeed, mDatabaseV3.numberKeyEncryptionRounds)
return mDatabaseV3.finalKey
} catch (e: IOException) {
throw PwDbOutputException("Key creation failed.", e)
throw DatabaseOutputException("Key creation failed.", e)
}
}
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
override fun output() {
// 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
@@ -74,7 +74,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
throw Exception()
}
} catch (e: Exception) {
throw PwDbOutputException("Algorithm not supported.", e)
throw DatabaseOutputException("Algorithm not supported.", e)
}
try {
@@ -86,23 +86,23 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
bos.close()
} catch (e: InvalidKeyException) {
throw PwDbOutputException("Invalid key", e)
throw DatabaseOutputException("Invalid key", e)
} catch (e: InvalidAlgorithmParameterException) {
throw PwDbOutputException("Invalid algorithm parameter.", e)
throw DatabaseOutputException("Invalid algorithm parameter.", e)
} 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 {
val random = super.setIVs(header)
random.nextBytes(header.transformSeed)
return random
}
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): PwDbHeaderV3 {
// Build header
val header = PwDbHeaderV3()
@@ -115,7 +115,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
} else if (mDatabaseV3.encryptionAlgorithm === PwEncryptionAlgorithm.Twofish) {
header.flags = header.flags or PwDbHeaderV3.FLAG_TWOFISH
} else {
throw PwDbOutputException("Unsupported algorithm.")
throw DatabaseOutputException("Unsupported algorithm.")
}
header.version = PwDbHeaderV3.DBVER_DW
@@ -130,7 +130,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
messageDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("SHA-256 not implemented here.", e)
throw DatabaseOutputException("SHA-256 not implemented here.", e)
}
// Header checksum
@@ -138,7 +138,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
headerDigest = MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("SHA-256 not implemented here.", e)
throw DatabaseOutputException("SHA-256 not implemented here.", e)
}
var nos = NullOutputStream()
@@ -151,7 +151,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
pho.outputEnd()
headerDos.flush()
} catch (e: IOException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
}
val headerHash = headerDigest.digest()
@@ -166,7 +166,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
bos.flush()
bos.close()
} catch (e: IOException) {
throw PwDbOutputException("Failed to generate checksum.", e)
throw DatabaseOutputException("Failed to generate checksum.", e)
}
header.contentsHash = messageDigest!!.digest()
@@ -181,14 +181,14 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
pho.outputEnd()
dos.flush()
} catch (e: IOException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
}
return header
}
@Suppress("CAST_NEVER_SUCCEEDS")
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
fun outputPlanGroupAndEntries(os: OutputStream) {
val los = LEDataOutputStream(os)
@@ -199,7 +199,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
los.writeInt(headerHashBlock!!.size)
los.write(headerHashBlock!!)
} 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 {
pgo.output()
} catch (e: IOException) {
throw PwDbOutputException("Failed to output a tree", e)
throw DatabaseOutputException("Failed to output a tree", e)
}
}
mDatabaseV3.doForEachEntryInIndex { entry ->
@@ -217,7 +217,7 @@ class PwDbV3Output(private val mDatabaseV3: PwDatabaseV3, os: OutputStream) : Pw
try {
peo.output()
} 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.database.*
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.file.PwCompressionAlgorithm
import com.kunzisoft.keepass.database.element.PwCompressionAlgorithm
import com.kunzisoft.keepass.database.file.PwDbHeaderV4
import com.kunzisoft.keepass.database.element.security.ProtectedBinary
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 engine: CipherEngine? = null
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
override fun output() {
try {
try {
engine = CipherFactory.getInstance(mDatabaseV4.dataCipher)
} catch (e: NoSuchAlgorithmException) {
throw PwDbOutputException("No such cipher", e)
throw DatabaseOutputException("No such cipher", e)
}
header = outputHeader(mOS)
@@ -91,10 +91,9 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
val osXml: OutputStream
try {
if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
osXml = GZIPOutputStream(osPlain)
} else {
osXml = osPlain
osXml = when(mDatabaseV4.compressionAlgorithm) {
PwCompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
else -> osPlain
}
if (header!!.version >= PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -105,13 +104,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
outputDatabase(osXml)
osXml.close()
} catch (e: IllegalArgumentException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
} catch (e: IllegalStateException) {
throw PwDbOutputException(e)
throw DatabaseOutputException(e)
}
} 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)
}
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
private fun attachStreamEncryptor(header: PwDbHeaderV4, os: OutputStream): CipherOutputStream {
val cipher: Cipher
try {
@@ -237,13 +236,13 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseV4.finalKey!!, header.encryptionIV)
} catch (e: Exception) {
throw PwDbOutputException("Invalid algorithm.", e)
throw DatabaseOutputException("Invalid algorithm.", e)
}
return CipherOutputStream(os, cipher)
}
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
override fun setIVs(header: PwDbHeaderV4): SecureRandom {
val random = super.setIVs(header)
random.nextBytes(header.masterSeed)
@@ -259,7 +258,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
}
try {
val kdf = KdfFactory.getEngineV4(mDatabaseV4.kdfParameters)
val kdf = mDatabaseV4.getEngineV4(mDatabaseV4.kdfParameters)
kdf.randomize(mDatabaseV4.kdfParameters!!)
} catch (unknownKDF: 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)
if (randomStream == null) {
throw PwDbOutputException("Invalid random cipher")
throw DatabaseOutputException("Invalid random cipher")
}
if (header.version < PwDbHeaderV4.FILE_VERSION_32_4) {
@@ -286,7 +285,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
return random
}
@Throws(PwDbOutputException::class)
@Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): PwDbHeaderV4 {
val header = PwDbHeaderV4(mDatabaseV4)
@@ -296,7 +295,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
try {
pho.output()
} catch (e: IOException) {
throw PwDbOutputException("Failed to output the header.", e)
throw DatabaseOutputException("Failed to output the header.", e)
}
hashOfHeader = pho.hashOfHeader
@@ -403,7 +402,7 @@ class PwDbV4Output(private val mDatabaseV4: PwDatabaseV4, outputStream: OutputSt
}
} else {
if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.Gzip) {
if (mDatabaseV4.getCompressionAlgorithm() == PwCompressionAlgorithm.GZip) {
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)))
} else {
if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.Gzip) {
if (mDatabaseV4.compressionAlgorithm === PwCompressionAlgorithm.GZip) {
xml.attribute(null, PwDatabaseV4XML.AttrCompressed, PwDatabaseV4XML.ValTrue)
val compressData = MemoryUtil.compress(buffer)

View File

@@ -66,7 +66,6 @@ open class Education(val activity: Activity) {
val educationResourcesKeys = intArrayOf(
R.string.education_create_db_key,
R.string.education_select_db_key,
R.string.education_open_link_db_key,
R.string.education_unlock_key,
R.string.education_read_only_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))
}
/**
* 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.
*

View File

@@ -72,31 +72,4 @@ class FileDatabaseSelectActivityEducation(activity: Activity)
},
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 {
fragmentManager?.let { fragmentManager ->
AssignMasterKeyDialogFragment().show(fragmentManager, "passwordDialog")
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_SECURITY)
false
}
}
findPreference<Preference>(getString(R.string.settings_database_credentials_key))?.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DATABASE_MASTER_KEY)
false
}
}

View File

@@ -22,36 +22,37 @@ package com.kunzisoft.keepass.settings
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.Resources
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.autofill.AutofillManager
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager
import androidx.fragment.app.DialogFragment
import androidx.preference.*
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.KeyboardExplanationDialogFragment
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.dialogs.*
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
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.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.settings.preference.DialogListExplanationPreference
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.settings.preference.InputNumberPreference
import com.kunzisoft.keepass.settings.preference.InputTextPreference
import com.kunzisoft.keepass.settings.preference.*
import com.kunzisoft.keepass.settings.preference.DialogColorPreference.Companion.DISABLE_COLOR
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
@@ -61,12 +62,13 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
private var mCount = 0
private var mRoundPref: InputNumberPreference? = null
private var mMemoryPref: InputNumberPreference? = null
private var mParallelismPref: InputNumberPreference? = null
private var databaseCustomColorPref: DialogColorPreference? = null
private var mRoundPref: InputKdfNumberPreference? = null
private var mMemoryPref: InputKdfNumberPreference? = null
private var mParallelismPref: InputKdfNumberPreference? = null
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() {
@@ -74,7 +76,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
activity?.let { activity ->
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) {
val autofillManager = activity.getSystemService(AutofillManager::class.java)
autoFillEnablePreference.isChecked = autofillManager != null
@@ -110,6 +112,12 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
Screen.DATABASE -> {
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)
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) {
val autofillManager = activity.getSystemService(AutofillManager::class.java)
if (autofillManager != null && autofillManager.hasEnabledAutofillServices())
@@ -217,7 +225,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
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
var biometricUnlockSupported = false
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) {
deleteKeysFingerprints?.isEnabled = false
} else {
@@ -338,26 +346,54 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
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
val dbNamePref: InputTextPreference? = findPreference<InputTextPreference>(getString(R.string.database_name_key))
// Database name
val dbNamePref: InputTextPreference? = findPreference(getString(R.string.database_name_key))
if (mDatabase.containsName()) {
dbNamePref?.summary = mDatabase.name
} else {
dbGeneralPrefCategory?.removePreference(dbNamePref)
}
// Db description
val dbDescriptionPref: InputTextPreference? = findPreference<InputTextPreference>(getString(R.string.database_description_key))
// Database description
val dbDescriptionPref: InputTextPreference? = findPreference(getString(R.string.database_description_key))
if (mDatabase.containsDescription()) {
dbDescriptionPref?.summary = mDatabase.description
} else {
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
val recycleBinPref: SwitchPreference? = findPreference<SwitchPreference>(getString(R.string.recycle_bin_key))
val recycleBinPref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_key))
// TODO Recycle
dbGeneralPrefCategory?.removePreference(recycleBinPref) // To delete
if (mDatabase.isRecycleBinAvailable) {
@@ -371,6 +407,26 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
findPreference<Preference>(getString(R.string.database_version_key))
?.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
findPreference<DialogListExplanationPreference>(getString(R.string.encryption_algorithm_key))
?.summary = mDatabase.getEncryptionAlgorithmName(resources)
@@ -380,24 +436,41 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
?.summary = mDatabase.getKeyDerivationName(resources)
// Round encryption
mRoundPref = findPreference<InputNumberPreference>(getString(R.string.transform_rounds_key))
mRoundPref?.summary = mDatabase.numberKeyEncryptionRoundsAsString
mRoundPref = findPreference(getString(R.string.transform_rounds_key))
mRoundPref?.summary = mDatabase.numberKeyEncryptionRounds.toString()
// Memory Usage
mMemoryPref = findPreference<InputNumberPreference>(getString(R.string.memory_usage_key))
mMemoryPref?.summary = mDatabase.memoryUsageAsString
mMemoryPref = findPreference(getString(R.string.memory_usage_key))
mMemoryPref?.summary = mDatabase.memoryUsage.toString()
// Parallelism
mParallelismPref = findPreference<InputNumberPreference>(getString(R.string.parallelism_key))
mParallelismPref?.summary = mDatabase.parallelismAsString
mParallelismPref = findPreference(getString(R.string.parallelism_key))
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 {
Log.e(javaClass.name, "Database isn't ready")
}
}
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 ->
if (newValue as Boolean && context != null) {
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?) {
var otherDialogFragment = false
@@ -461,6 +555,23 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
preference.key == getString(R.string.database_description_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) -> {
dialogFragment = DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.newInstance(preference.key)
}
@@ -486,7 +597,7 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
if (dialogFragment != null && !mDatabaseReadOnly) {
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(fragmentManager, null)
dialogFragment.show(fragmentManager, TAG_PREF_FRAGMENT)
}
// Could not be handled here. Try with the super method.
else if (otherDialogFragment) {
@@ -510,6 +621,8 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
private const val TAG_KEY = "NESTED_KEY"
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
private const val REQUEST_CODE_AUTOFILL = 5201
@JvmOverloads
@@ -529,8 +642,10 @@ class NestedSettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferen
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_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.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)
}
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 {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.omitbackup_key),
@@ -109,8 +115,8 @@ object PreferencesUtil {
fun getAppTimeout(context: Context): Long {
return try {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
java.lang.Long.parseLong(prefs.getString(context.getString(R.string.app_timeout_key),
context.getString(R.string.clipboard_timeout_default)) ?: "60000")
(prefs.getString(context.getString(R.string.app_timeout_key),
context.getString(R.string.clipboard_timeout_default)) ?: "300000").toLong()
} catch (e: NumberFormatException) {
TimeoutHelper.DEFAULT_TIMEOUT
}
@@ -185,12 +191,6 @@ object PreferencesUtil {
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 {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.allow_copy_password_first_time_key),

View File

@@ -115,7 +115,7 @@ open class SettingsActivity
true)
}
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword)) {
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
progressDialogThread.start()
} else {
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
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
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,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputTextExplanationPreference(context, attrs, defStyleAttr, defStyleRes) {
// Save to Shared Preferences
var number: Long = 0
set(number) {
field = number
persistLong(number)
}
: InputTextPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun getDialogLayoutResource(): Int {
return R.layout.pref_dialog_numbers
return R.layout.pref_dialog_input_numbers
}
override fun setSummary(summary: CharSequence) {
if (summary == KdfEngine.UNKNOWN_VALUE_STRING) {
isEnabled = false
if (summary == INFINITE_VALUE_STRING) {
super.setSummary("")
} else {
isEnabled = true
super.setSummary(summary)
}
}
override fun onGetDefaultValue(a: TypedArray?, index: Int): Any {
// Default value from attribute. Fallback value is set to 0.
return a?.getInt(index, 0) ?: 0
companion object {
const val INFINITE_VALUE_STRING = "-1"
}
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,7 +6,7 @@ import android.util.AttributeSet
import com.kunzisoft.keepass.R
class InputTextPreference @JvmOverloads constructor(context: Context,
open class InputTextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.content.res.Resources
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
@@ -36,10 +37,14 @@ abstract class DatabaseSavePreferenceDialogFragmentCompat : InputPreferenceDialo
protected lateinit var settingsResources: Resources
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.database = Database.getInstance()
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
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
import android.view.View
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.preference.PreferenceDialogFragmentCompat
import android.view.View
import android.widget.TextView
import com.kunzisoft.keepass.R
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
private var inputTextView: EditText? = 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?
get() = textExplanationView?.text?.toString() ?: ""
set(explanationText) {
textExplanationView?.apply {
if (explanationText != null && explanationText.isNotEmpty()) {
textExplanationView?.text = explanationText
textExplanationView?.visibility = View.VISIBLE
text = explanationText
visibility = View.VISIBLE
} else {
textExplanationView?.text = explanationText
textExplanationView?.visibility = View.VISIBLE
text = ""
visibility = View.GONE
}
}
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
inputTextView = view.findViewById(R.id.input_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) {
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,39 +21,37 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.tasks.ActionRunnable
class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDialogFragmentCompat() {
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
setExplanationText(R.string.memory_usage_explanation)
inputText = database?.memoryUsageAsString ?: ""
inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
}
override fun onDialogClosed(positiveResult: Boolean) {
if (database != null && positiveResult) {
var memoryUsage: Long
try {
val stringMemory = inputText
memoryUsage = java.lang.Long.parseLong(stringMemory)
if (positiveResult) {
database?.let { database ->
var memoryUsage: Long = try {
inputText.toLong()
} catch (e: NumberFormatException) {
Toast.makeText(context, R.string.error_rounds_not_number, Toast.LENGTH_LONG).show() // TODO change error
return
MIN_MEMORY_USAGE
}
if (memoryUsage < 1) {
memoryUsage = 1
if (memoryUsage < MIN_MEMORY_USAGE) {
memoryUsage = MIN_MEMORY_USAGE
}
// TODO Max Memory
val oldMemoryUsage = database!!.memoryUsage
database!!.memoryUsage = memoryUsage
val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage
actionInUIThreadAfterSaveDatabase = AfterMemorySave(memoryUsage, oldMemoryUsage)
}
}
super.onDialogClosed(positiveResult)
}
@@ -63,9 +61,12 @@ class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDia
: ActionRunnable() {
override fun onFinishRun(result: Result) {
val memoryToShow = mNewMemory
if (!result.isSuccess) {
val memoryToShow =
if (result.isSuccess) {
mNewMemory
} else {
database?.memoryUsage = mOldMemory
mOldMemory
}
preference.summary = memoryToShow.toString()
}
@@ -73,6 +74,8 @@ class MemoryUsagePreferenceDialogFragmentCompat : InputDatabaseSavePreferenceDia
companion object {
const val MIN_MEMORY_USAGE = 1L
fun newInstance(key: String): MemoryUsagePreferenceDialogFragmentCompat {
val fragment = MemoryUsagePreferenceDialogFragmentCompat()
val bundle = Bundle(1)

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
/**
* 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,
* 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.exception = exception
result.message = message
if (isSuccess || executeNestedActionIfResultFalse) {
execute()
@@ -89,5 +103,8 @@ abstract class ActionRunnable(private var nestedActionRunnable: 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),
context.getString(R.string.clipboard_timeout_default))
val clipClearTime = java.lang.Long.parseLong(sClipClear ?: "60000")
val clipClearTime = (sClipClear ?: "300000").toLong()
if (clipClearTime > 0) {
mTimer.schedule(ClearClipboardTask(context, text), clipClearTime)
}

View File

@@ -20,7 +20,6 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import androidx.core.content.ContextCompat
import android.text.method.PasswordTransformationMethod
import android.util.AttributeSet
import android.view.LayoutInflater
@@ -29,9 +28,14 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
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.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 java.text.DateFormat
import java.util.*
class EntryContentsView @JvmOverloads constructor(context: Context,
@@ -59,9 +63,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val extrasContainerView: View
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 modificationDateView: TextView
private val lastAccessDateView: TextView
@@ -69,6 +70,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val uuidView: TextView
private val historyContainerView: View
private val historyListView: RecyclerView
private val historyAdapter = EntryHistoryAdapter(context)
val isUserNamePresent: Boolean
get() = userNameContainerView.visibility == View.VISIBLE
@@ -103,6 +108,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
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 taColorAccent = context.theme.obtainStyledAttributes(attrColorAccent)
colorAccent = taColorAccent.getColor(0, Color.BLACK)
@@ -230,24 +242,20 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
extrasContainerView.visibility = View.GONE
}
private fun getDateTime(date: Date): String {
return dateFormat.format(date) + " " + timeFormat.format(date)
fun assignCreationDate(date: PwDate) {
creationDateView.text = date.getDateTimeString(resources)
}
fun assignCreationDate(date: Date) {
creationDateView.text = getDateTime(date)
fun assignModificationDate(date: PwDate) {
modificationDateView.text = date.getDateTimeString(resources)
}
fun assignModificationDate(date: Date) {
modificationDateView.text = getDateTime(date)
fun assignLastAccessDate(date: PwDate) {
lastAccessDateView.text = date.getDateTimeString(resources)
}
fun assignLastAccessDate(date: Date) {
lastAccessDateView.text = getDateTime(date)
}
fun assignExpiresDate(date: Date) {
expiresDateView.text = getDateTime(date)
fun assignExpiresDate(date: PwDate) {
expiresDateView.text = date.getDateTimeString(resources)
}
fun assignExpiresDate(constString: String) {
@@ -258,6 +266,21 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
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 {
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.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.Field
import java.util.HashMap
class EntryEditContentsView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -168,9 +169,11 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
* Add a new view to fill in the information of the customized field
*/
fun addNewCustomField(name: String = "", value: ProtectedString = ProtectedString(false, "")) {
val entryEditCustomField = EntryEditCustomField(context)
entryEditCustomField.setData(name, value)
entryEditCustomField.setFontVisibility(fontInVisibility)
val entryEditCustomField = EntryEditCustomField(context).apply {
setData(name, value)
setFontVisibility(fontInVisibility)
requestFocus()
}
entryExtraFieldsContainer.addView(entryEditCustomField)
}

View File

@@ -19,13 +19,17 @@
*/
package com.kunzisoft.keepass.view
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Activity
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Typeface
import com.google.android.material.snackbar.Snackbar
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
/**
@@ -56,3 +60,40 @@ fun Activity.lockScreenOrientation() {
fun Activity.unlockScreenOrientation() {
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.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/entry_scroll"
android:layout_width="match_parent"
@@ -86,12 +85,30 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="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
android:id="@+id/entry_contents"
android:layout_height="match_parent"
android:layout_width="0dp"
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_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -182,7 +182,7 @@
app:layout_constraintBottom_toBottomOf="parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/browse_button"
android:id="@+id/open_database_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/select_database_file"
@@ -194,72 +194,10 @@
android:paddingStart="32dp"
android:paddingRight="24dp"
android:paddingEnd="24dp"
app:layout_constraintBottom_toTopOf="@+id/file_select_expandable"/>
<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>
app:layout_constraintBottom_toTopOf="@+id/create_database_button"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/create_database"
android:id="@+id/create_database_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"

View File

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

View File

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

View File

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

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