Merge branch 'feature/Hardware_Key' into develop

This commit is contained in:
J-Jamet
2022-08-08 14:00:21 +02:00
78 changed files with 2441 additions and 1050 deletions

View File

@@ -0,0 +1,90 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hardwareKey",
"columnName": "hardware_key",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')"
]
}
}

View File

@@ -55,7 +55,8 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -155,8 +156,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity(
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri
databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
)
}
}
@@ -250,7 +252,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
?: MainCredential()
databaseFilesViewModel.addDatabaseFile(
databaseUri,
mainCredential.keyFileUri
mainCredential.keyFileUri,
mainCredential.hardwareKey
)
}
}
@@ -297,10 +300,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this,
databaseUri,
keyFile,
hardwareKey,
{ exception ->
fileNoFoundAction(exception)
},
@@ -321,7 +325,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null)
launchPasswordActivity(databaseUri, null, null)
// Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0)

View File

@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK

View File

@@ -56,9 +56,11 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
@@ -101,6 +103,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var mRememberHardwareKey: Boolean = false
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
@@ -133,11 +137,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
PreferencesUtil.enableReadOnlyDatabase(this)
}
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
// Build elements to manage keyfile selection
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
@@ -171,6 +177,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onKeyFileChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onHardwareKeyChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
// Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -204,10 +220,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
databaseKeyFileUri
}
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
val hardwareKey =
if (mRememberHardwareKey
&& databaseHardwareKey == null) {
databaseFile?.hardwareKey
} else {
databaseHardwareKey
}
// Define title
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
}
}
@@ -215,6 +240,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
super.onResume()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
// Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
@@ -332,24 +358,36 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun getUriFromIntent(intent: Intent?) {
// If is a view intent
val action = intent?.action
if (action != null
&& action == VIEW_INTENT) {
mDatabaseFileUri = intent.data
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
if (action == VIEW_INTENT) {
fillCredentials(
intent.data,
UriUtil.getUriFromIntent(intent, KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
)
} else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
mainCredentialView?.populateKeyFileTextView(it)
}
fillCredentials(
intent?.getParcelableExtra(KEY_FILENAME),
intent?.getParcelableExtra(KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
)
}
try {
intent?.removeExtra(KEY_KEYFILE)
intent?.removeExtra(KEY_HARDWARE_KEY)
} catch (e: Exception) {}
mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
}
}
private fun fillCredentials(databaseUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
mDatabaseFileUri = databaseUri
mainCredentialView?.populateKeyFileView(keyFileUri)
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
getUriFromIntent(intent)
@@ -358,7 +396,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun launchGroupActivityIfLoaded(database: Database) {
// Check if database really loaded
if (database.loaded) {
clearCredentialsViews(true)
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
GroupActivity.launch(this,
database,
{ onValidateSpecialMode() },
@@ -408,7 +446,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
}
CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file
@@ -423,14 +461,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
)
}
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
// Define Key File text
if (mRememberKeyFile) {
mainCredentialView?.populateKeyFileTextView(keyFileUri)
mainCredentialView?.populateKeyFileView(keyFileUri)
}
// Define hardware key
if (mRememberHardwareKey) {
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
// Define listener for validate button
confirmButtonView?.setOnClickListener { loadDatabase() }
confirmButtonView?.setOnClickListener {
mainCredentialView?.validateCredential()
}
// If Activity is launch with a password and want to open directly
val intent = intent
@@ -462,10 +509,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
}
}
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
clearHardwareKey: Boolean = !mRememberHardwareKey) {
mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) {
mainCredentialView?.populateKeyFileTextView(null)
mainCredentialView?.populateKeyFileView(null)
}
if (clearHardwareKey) {
mainCredentialView?.populateHardwareKeyView(null)
}
}
@@ -656,18 +707,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val KEY_HARDWARE_KEY = "hardwareKey"
private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
private fun buildAndLaunchIntent(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile)
if (hardwareKey != null)
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
intentBuildLauncher.invoke(intent)
}
@@ -680,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
@Throws(FileNotFoundException::class)
fun launch(activity: Activity,
databaseFile: Uri,
keyFile: Uri?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
keyFile: Uri?,
hardwareKey: HardwareKey?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent)
}
}
@@ -696,8 +754,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSearchResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult(
activity,
intent,
@@ -715,8 +774,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSaveResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult(
activity,
intent,
@@ -734,8 +794,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity,
intent,
@@ -754,10 +815,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
@@ -775,8 +837,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForRegistration(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
activity,
intent,
@@ -792,6 +855,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
@@ -800,43 +864,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
MainCredentialActivity.launch(activity,
databaseUri, keyFile)
launch(
activity,
databaseUri,
keyFile,
hardwareKey
)
},
{ searchInfo -> // Search Action
MainCredentialActivity.launchForSearchResult(activity,
databaseUri, keyFile,
searchInfo)
launchForSearchResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Save Action
MainCredentialActivity.launchForSaveResult(activity,
databaseUri, keyFile,
searchInfo)
launchForSaveResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo -> // Keyboard Selection Action
MainCredentialActivity.launchForKeyboardResult(activity,
databaseUri, keyFile,
searchInfo)
launchForKeyboardResult(
activity,
databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode()
},
{ searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
MainCredentialActivity.launchForAutofillResult(activity,
databaseUri, keyFile,
autofillActivityResultLauncher,
autofillComponent,
searchInfo)
launchForAutofillResult(
activity,
databaseUri,
keyFile,
hardwareKey,
autofillActivityResultLauncher,
autofillComponent,
searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
{ registerInfo -> // Registration Action
MainCredentialActivity.launchForRegistration(activity,
databaseUri, keyFile,
registerInfo)
launchForRegistration(
activity,
databaseUri,
keyFile,
hardwareKey,
registerInfo
)
onLaunchActivitySpecialMode()
}
)

View File

@@ -27,7 +27,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.MainCredentialView
@@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri)
mainCredentialView?.populateKeyFileView(uri)
}
}
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)

View File

@@ -26,7 +26,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() {

View File

@@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
UriUtil.gotoUrl(activity,
activity.getString(R.string.play_store_url,
activity.getString(R.string.keepro_app_id))
)
}
} else {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
UriUtil.gotoUrl(activity, R.string.contribution_url)
}
}
builder.setMessage(stringBuilder)

View File

@@ -35,9 +35,12 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility
@@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null
private var mKeyFileUri: Uri? = null
private var mHardwareKey: HardwareKey? = null
private var rootView: View? = null
private lateinit var rootView: View
private var passwordCheckBox: CompoundButton? = null
private lateinit var passwordCheckBox: CompoundButton
private lateinit var passwordView: PassKeyView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
private var passKeyView: PassKeyView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
private lateinit var keyFileCheckBox: CompoundButton
private lateinit var keyFileSelectionView: KeyFileSelectionView
private var keyFileCheckBox: CompoundButton? = null
private var keyFileSelectionView: KeyFileSelectionView? = null
private lateinit var hardwareKeyCheckBox: CompoundButton
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
private var mListener: AssignMainCredentialDialogListener? = null
@@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private var mAllowNoMasterKey: Boolean = false
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true
passwordCheckBox.isChecked = true
}
}
@@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
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)
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
}
val builder = AlertDialog.Builder(activity)
@@ -128,63 +135,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passKeyView = rootView?.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility()
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passwordView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
keyFileSelectionView.error = null
keyFileCheckBox.isChecked = true
keyFileSelectionView.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { hardwareKey ->
hardwareKeyCheckBox.isChecked = true
hardwareKeySelectionView.error =
if (!HardwareKeyResponseHelper.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
// show hardware driver dialog if required
getString(R.string.error_driver_required, hardwareKey.toString())
} else {
null
}
}
val dialog = builder.create()
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
if (passwordCheckBox != null && keyFileCheckBox!= null) {
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
mMasterPassword = ""
mKeyFileUri = null
mHardwareKey = null
mMasterPassword = ""
mKeyFile = null
var error = verifyPassword() || verifyKeyFile()
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
error = true
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
approveMainCredential()
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
}
@@ -194,67 +201,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
private fun approveMainCredential() {
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
val errorHardwareKey = verifyHardwareKey()
// Check all to fill error
var error = errorPassword || errorKeyFile || errorHardwareKey
val hardwareKey = hardwareKeySelectionView.hardwareKey
if (!error
&& (!passwordCheckBox.isChecked)
&& (!keyFileCheckBox.isChecked)
&& (!hardwareKeyCheckBox.isChecked)
) {
error = true
if (mAllowNoMasterKey) {
// show no key dialog if required
showNoKeyConfirmationDialog()
} else {
passwordRepeatTextInputLayout.error =
getString(R.string.error_disallow_no_credentials)
}
} else if (!error
&& mMasterPassword.isNullOrEmpty()
&& !keyFileCheckBox.isChecked
&& !hardwareKeyCheckBox.isChecked
) {
// show empty password dialog if required
error = true
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyResponseHelper.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
) {
// show hardware driver dialog if required
error = true
hardwareKeySelectionView.error =
getString(R.string.error_driver_required, hardwareKey.toString())
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
private fun verifyPassword(): Boolean {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passwordView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
keyFileSelectionView.error = null
if (keyFileCheckBox.isChecked) {
keyFileSelectionView.uri?.let { uri ->
mKeyFileUri = uri
} ?: run {
error = true
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
}
}
return error
}
private fun verifyHardwareKey(): Boolean {
var error = false
hardwareKeySelectionView.error = null
if (hardwareKeyCheckBox.isChecked) {
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
mHardwareKey = hardwareKey
} ?: run {
error = true
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
}
}
return error
}
private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
return MainCredential(masterPassword, keyFile)
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
}
override fun onResume() {
super.onResume()
// To check checkboxes if a text is present
passKeyView?.addTextChangedListener(passwordTextWatcher)
passwordView.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passKeyView?.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passKeyView != null
&& passwordRepeatView != null) {
mMasterPassword = passKeyView!!.passwordString
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
}
if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true
showEmptyPasswordConfirmationDialog()
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
keyFileSelectionView?.uri?.let { uri ->
mKeyFile = uri
} ?: run {
error = true
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
}
}
return error
passwordView.removeTextChangedListener(passwordTextWatcher)
}
private fun showEmptyPasswordConfirmationDialog() {
@@ -262,10 +315,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@SetMainCredentialDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create()
@@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
})
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox?.isChecked = false
keyFileSelectionView?.uri = null
keyFileCheckBox.isChecked = false
keyFileSelectionView.uri = null
}
mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show()

View File

@@ -39,6 +39,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
/*
if (UriUtil.contributingUser(activity)) {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
@@ -46,6 +47,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else {
*/
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
@@ -53,7 +55,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
}
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
}
//}
builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it
return builder.create()

View File

@@ -6,9 +6,10 @@ import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
@@ -17,10 +18,12 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null
private val mChallengeResponseViewModel: ChallengeResponseViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider = DatabaseTaskProvider(this, mChallengeResponseViewModel)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true
@@ -36,6 +39,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
}
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database
mDatabaseViewModel.defineDatabase(database)

View File

@@ -32,7 +32,6 @@ import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -44,7 +43,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable

View File

@@ -23,8 +23,15 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
import androidx.room.AutoMigration
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
@Database(
version = 2,
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
autoMigrations = [
AutoMigration (from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database
import android.content.Context
import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile(
databaseUri,
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists,
@@ -85,13 +87,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|| !hideBrokenLocations) {
databaseFileListLoaded.add(
DatabaseFile(
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
)
}
@@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute()
}
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
fun addOrUpdateDatabaseUri(databaseUri: Uri,
keyFileUri: Uri? = null,
hardwareKey: HardwareKey? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile(
databaseUri,
keyFileUri
databaseUri,
keyFileUri,
hardwareKey
), databaseFileAddedOrUpdatedResult)
}
@@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value,
System.currentTimeMillis()
)
@@ -147,13 +154,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
fileDatabaseHistory.databaseUri)
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
}
},
@@ -172,10 +180,11 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
if (returnValue > 0) {
DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
)
} else {
null

View File

@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
@ColumnInfo(name = "keyfile_uri")
var keyFileUri: String?,
@ColumnInfo(name = "hardware_key")
var hardwareKey: String?,
@ColumnInfo(name = "updated")
val updated: Long
) {

View File

@@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage
@@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
} ?: throw UnknownDatabaseLocationException()
} ?: throw Exception("AdvancedUnlockManager not initialized")
}

View File

@@ -24,15 +24,16 @@ import android.net.Uri
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.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
open class AssignMainCredentialInDatabaseRunnable (
context: Context,
database: Database,
protected val mDatabaseUri: Uri,
protected val mMainCredential: MainCredential)
: SaveDatabaseRunnable(context, database, true) {
context: Context,
database: Database,
protected val mDatabaseUri: Uri,
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) {
private var mBackupKey: ByteArray? = null
@@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable (
// Set key
try {
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
database.masterKey.copyInto(mBackupKey!!)
} catch (e: Exception) {
erase(mBackupKey)
setError(e)

View File

@@ -24,7 +24,8 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
class CreateDatabaseRunnable(context: Context,
@@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context,
private val databaseName: String,
private val rootName: String,
private val templateGroupName: String?,
mainCredential: MainCredential,
val mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val createDatabaseResult: ((Result) -> Unit)?)
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) {
override fun onStartRun() {
try {
@@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context,
// Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
.addOrUpdateDatabaseUri(
mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mainCredential.hardwareKey else null,
)
}
// Register the current time to init the lock timer

View File

@@ -38,12 +38,16 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
@@ -82,6 +86,7 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import kotlinx.coroutines.launch
import java.util.*
@@ -92,7 +97,6 @@ import java.util.*
class DatabaseTaskProvider {
private var activity: FragmentActivity? = null
private var service: Service? = null
private var context: Context
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
@@ -111,30 +115,80 @@ class DatabaseTaskProvider {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
constructor(activity: FragmentActivity) {
private var mChallengeResponseViewModel: ChallengeResponseViewModel? = null
constructor(activity: FragmentActivity,
challengeResponseViewModel: ChallengeResponseViewModel) {
this.activity = activity
this.context = activity
this.intentDatabaseTask = Intent(activity.applicationContext,
DatabaseTaskNotificationService::class.java)
// ViewModel used to keep response if activity recreated
this.mChallengeResponseViewModel = challengeResponseViewModel
// To manage hardware key challenge response
val hardwareKeyResponseHelper = HardwareKeyResponseHelper(activity)
hardwareKeyResponseHelper.buildHardwareKeyResponse { responseData, _ ->
// TODO Verify database
// Send to view model in case activity is restarted and not yet connected to service
challengeResponseViewModel.respond(responseData ?: ByteArray(0))
}
challengeResponseViewModel.dataResponded.observe(activity) { response ->
// Consume the response
if (response != null) {
val binder = mBinder
if (binder != null) {
binder.getService().respondToChallenge(response)
challengeResponseViewModel.consumeResponse()
}
}
}
this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) {
if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) {
hardwareKeyResponseHelper.launchChallengeForResponse(hardwareKey, seed)
} else {
throw InvalidCredentialsDatabaseException(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
}
}
}
}
constructor(service: Service) {
this.service = service
this.context = service
this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java)
}
fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null
this.onActionFinish = null
this.databaseTaskBroadcastReceiver = null
this.mBinder = null
this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
this.mChallengeResponseViewModel = null
}
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
startDialog(titleId, messageId, warningId)
override fun onStartAction(database: Database,
progressMessage: ProgressMessage) {
startDialog(progressMessage)
}
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
updateDialog(titleId, messageId, warningId)
override fun onUpdateAction(database: Database,
progressMessage: ProgressMessage) {
updateDialog(progressMessage)
}
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
override fun onStopAction(database: Database,
actionTask: String,
result: ActionRunnable.Result) {
onActionFinish?.invoke(database, actionTask, result)
// Remove the progress task
stopDialog()
@@ -181,9 +235,9 @@ class DatabaseTaskProvider {
}
}
private fun startDialog(titleId: Int? = null,
messageId: Int? = null,
warningId: Int? = null) {
private var requestChallengeListener: DatabaseTaskNotificationService.RequestChallengeListener? = null
private fun startDialog(progressMessage: ProgressMessage) {
activity?.let { activity ->
activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) {
@@ -197,22 +251,17 @@ class DatabaseTaskProvider {
PROGRESS_TASK_DIALOG_TAG
)
}
updateDialog(titleId, messageId, warningId)
updateDialog(progressMessage)
}
}
}
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
private fun updateDialog(progressMessage: ProgressMessage) {
progressTaskDialogFragment?.apply {
titleId?.let {
updateTitle(it)
}
messageId?.let {
updateMessage(it)
}
warningId?.let {
updateWarning(it)
}
updateTitle(progressMessage.titleId)
updateMessage(progressMessage.messageId)
updateWarning(progressMessage.warningId)
setCancellable(progressMessage.cancelable)
}
}
@@ -226,25 +275,38 @@ class DatabaseTaskProvider {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addDatabaseListener(databaseListener)
addDatabaseFileInfoListener(databaseInfoListener)
addActionTaskListener(actionTaskListener)
addServiceListeners(this)
getService().checkDatabase()
getService().checkDatabaseInfo()
getService().checkAction()
}
mChallengeResponseViewModel?.resendResponse()
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
removeServiceListeners(mBinder)
mBinder = null
}
}
}
}
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener)
service?.addActionTaskListener(actionTaskListener)
requestChallengeListener?.let {
service?.addRequestChallengeListener(it)
}
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener)
service?.removeDatabaseFileInfoListener(databaseInfoListener)
service?.removeDatabaseListener(databaseListener)
service?.removeRequestChallengeListener()
}
private fun bindService() {
initServiceConnection()
serviceConnection?.let {
@@ -262,10 +324,6 @@ class DatabaseTaskProvider {
serviceConnection = null
}
fun isBinded(): Boolean {
return mBinder != null
}
fun registerProgressTask() {
stopDialog()
@@ -299,9 +357,7 @@ class DatabaseTaskProvider {
fun unregisterProgressTask() {
stopDialog()
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
removeServiceListeners(mBinder)
mBinder = null
unBindService()
@@ -321,7 +377,7 @@ class DatabaseTaskProvider {
context.startService(intentDatabaseTask)
} catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
}
}
@@ -332,7 +388,8 @@ class DatabaseTaskProvider {
*/
fun startDatabaseCreate(databaseUri: Uri,
mainCredential: MainCredential) {
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
@@ -385,7 +442,8 @@ class DatabaseTaskProvider {
}
fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) {
mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)

View File

@@ -25,9 +25,10 @@ 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.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil
class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mUri: Uri,
private val mDatabaseUri: Uri,
private val mMainCredential: MainCredential,
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
private val mReadonly: Boolean,
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean,
@@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onActionRun() {
try {
mDatabase.loadData(mUri,
mMainCredential,
mReadonly,
context.contentResolver,
UriUtil.getBinaryDir(context),
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
mFixDuplicateUUID,
progressTaskUpdater)
mDatabase.loadData(
context.contentResolver,
mDatabaseUri,
mMainCredential,
mChallengeResponseRetriever,
mReadonly,
UriUtil.getBinaryDir(context),
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
mFixDuplicateUUID,
progressTaskUpdater
)
}
catch (e: LoadDatabaseException) {
catch (e: DatabaseInputException) {
setError(e)
}
@@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context,
// Save keyFile in app database
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
.addOrUpdateDatabaseUri(
mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null,
)
}
// Register the biometric

View File

@@ -22,9 +22,10 @@ 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.element.MainCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -33,6 +34,7 @@ class MergeDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mDatabaseToMergeUri: Uri?,
private val mDatabaseToMergeMainCredential: MainCredential?,
private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
@@ -43,15 +45,17 @@ class MergeDatabaseRunnable(private val context: Context,
override fun onActionRun() {
try {
mDatabase.mergeData(mDatabaseToMergeUri,
mDatabaseToMergeMainCredential,
mDatabase.mergeData(
context.contentResolver,
mDatabaseToMergeUri,
mDatabaseToMergeMainCredential,
mDatabaseToMergeChallengeResponseRetriever,
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
progressTaskUpdater
)
} catch (e: LoadDatabaseException) {
} catch (e: DatabaseException) {
setError(e)
}

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -47,7 +47,7 @@ class ReloadDatabaseRunnable(private val context: Context,
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
progressTaskUpdater)
} catch (e: LoadDatabaseException) {
} catch (e: DatabaseException) {
setError(e)
}

View File

@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
class RemoveUnlinkedDataDatabaseRunnable (
context: Context,
database: Database,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onActionRun() {
try {

View File

@@ -23,11 +23,15 @@ import android.content.Context
import android.net.Uri
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database,
private var saveDatabase: Boolean,
private var mainCredential: MainCredential?, // If null, uses composite Key
private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private var databaseCopyUri: Uri? = null)
: ActionRunnable() {
@@ -39,7 +43,12 @@ open class SaveDatabaseRunnable(protected var context: Context,
database.checkVersion()
if (saveDatabase && result.isSuccess) {
try {
database.saveData(databaseCopyUri, context.contentResolver)
database.saveData(
context.contentResolver,
context.cacheDir,
databaseCopyUri,
mainCredential,
challengeResponseRetriever)
} catch (e: DatabaseException) {
setError(e)
}

View File

@@ -22,14 +22,16 @@ package com.kunzisoft.keepass.database.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateCompressionBinariesDatabaseRunnable (
context: Context,
database: Database,
private val oldCompressionAlgorithm: CompressionAlgorithm,
private val newCompressionAlgorithm: CompressionAlgorithm,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() {
// Set new compression

View File

@@ -23,14 +23,16 @@ import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.hardware.HardwareKey
class DeleteEntryHistoryDatabaseRunnable (
context: Context,
database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
saveDatabase: Boolean)
: SaveDatabaseRunnable(context, database, saveDatabase) {
saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() {
try {

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
class RestoreEntryHistoryDatabaseRunnable (
@@ -30,7 +31,8 @@ class RestoreEntryHistoryDatabaseRunnable (
private val database: Database,
private val mainEntry: Entry,
private val entryHistoryPosition: Int,
private val saveDatabase: Boolean)
private val saveDatabase: Boolean,
private val challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionRunnable() {
private var updateEntryRunnable: UpdateEntryRunnable? = null
@@ -43,12 +45,15 @@ class RestoreEntryHistoryDatabaseRunnable (
historyToRestore.addEntryToHistory(it)
}
// Update the entry with the fresh formatted entry to restore
updateEntryRunnable = UpdateEntryRunnable(context,
database,
mainEntry,
historyToRestore,
saveDatabase,
null)
updateEntryRunnable = UpdateEntryRunnable(
context,
database,
mainEntry,
historyToRestore,
saveDatabase,
null,
challengeResponseRetriever
)
updateEntryRunnable?.onStartRun()

View File

@@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
abstract class ActionNodeDatabaseRunnable(
context: Context,
database: Database,
private val afterActionNodesFinish: AfterActionNodesFinish?,
save: Boolean)
: SaveDatabaseRunnable(context, database, save) {
save: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) {
/**
* Function do to a node action

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddEntryRunnable constructor(
context: Context,
@@ -31,8 +32,9 @@ class AddEntryRunnable constructor(
private val mNewEntry: Entry,
private val mParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
mNewEntry.touch(modified = true, touchParents = true)

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddGroupRunnable constructor(
context: Context,
@@ -30,8 +31,9 @@ class AddGroupRunnable constructor(
private val mNewGroup: Group,
private val mParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
mNewGroup.touch(modified = true, touchParents = true)

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context
import android.util.Log
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class CopyNodesRunnable constructor(
context: Context,
@@ -33,8 +36,9 @@ class CopyNodesRunnable constructor(
private val mNodesToCopy: List<Node>,
private val mNewParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mEntriesCopied = ArrayList<Entry>()

View File

@@ -20,16 +20,20 @@
package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
class DeleteNodesRunnable(context: Context,
database: Database,
private val mNodesToDelete: List<Node>,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null
private var mCanRecycle: Boolean = false

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context
import android.util.Log
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class MoveNodesRunnable constructor(
context: Context,
@@ -33,8 +36,9 @@ class MoveNodesRunnable constructor(
private val mNodesToMove: List<Node>,
private val mNewParent: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateEntryRunnable constructor(
context: Context,
@@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor(
private val mOldEntry: Entry,
private val mNewEntry: Entry,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
if (mOldEntry.nodeId == mNewEntry.nodeId) {

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateGroupRunnable constructor(
context: Context,
@@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor(
private val mOldGroup: Group,
private val mNewGroup: Group,
save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
afterActionNodesFinish: AfterActionNodesFinish?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() {
if (mOldGroup.nodeId == mNewGroup.nodeId) {

View File

@@ -0,0 +1,34 @@
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.hardware.HardwareKey
data class CompositeKey(var passwordData: ByteArray? = null,
var keyFileData: ByteArray? = null,
var hardwareKey: HardwareKey? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CompositeKey
if (passwordData != null) {
if (other.passwordData == null) return false
if (!passwordData.contentEquals(other.passwordData)) return false
} else if (other.passwordData != null) return false
if (keyFileData != null) {
if (other.keyFileData == null) return false
if (!keyFileData.contentEquals(other.keyFileData)) return false
} else if (other.keyFileData != null) return false
if (hardwareKey != other.hardwareKey) return false
return true
}
override fun hashCode(): Int {
var result = passwordData?.contentHashCode() ?: 0
result = 31 * result + (keyFileData?.contentHashCode() ?: 0)
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
return result
}
}

View File

@@ -54,12 +54,10 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import com.kunzisoft.keepass.utils.*
import java.io.*
import java.util.*
@@ -73,7 +71,7 @@ class Database {
var fileUri: Uri? = null
private set
private var mSearchHelper: SearchHelper? = null
private var mSearchHelper: SearchHelper = SearchHelper()
var isReadOnly = false
@@ -384,10 +382,14 @@ class Database {
set(masterKey) {
mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey
mDatabaseKDBX?.keyLastChanged = DateInstant()
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val transformSeed: ByteArray?
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
var rootGroup: Group?
get() {
mDatabaseKDB?.rootGroup?.let {
@@ -557,79 +559,28 @@ class Database {
this.dataModifiedSinceLastLoading = false
}
@Throws(LoadDatabaseException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB,
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
var databaseInputStream: InputStream? = null
try {
// Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
?: throw IOException("Database input stream cannot be retrieve")
databaseInputStream = BufferedInputStream(databaseStream)
if (!databaseInputStream.markSupported()) {
throw IOException("Input stream does not support mark.")
}
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
// Header not recognized
else -> throw SignatureDatabaseException()
}
this.mSearchHelper = SearchHelper()
loaded = true
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
databaseInputStream?.close()
}
}
@Throws(LoadDatabaseException::class)
fun loadData(uri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@Throws(DatabaseInputException::class)
fun loadData(
contentResolver: ContentResolver,
databaseUri: Uri,
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
readOnly: Boolean,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
// Save database URI
this.fileUri = uri
this.fileUri = databaseUri
// Check if the file is writable
this.isReadOnly = readOnly
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try {
// Get keyFile inputStream
mainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
// Read database stream for the first time
readDatabaseStream(contentResolver, uri,
readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream ->
val databaseKDB = DatabaseKDB().apply {
binaryCache.cacheDirectory = cacheDirectory
@@ -639,12 +590,12 @@ class Database {
.openDatabase(databaseInputStream,
progressTaskUpdater
) {
databaseKDB.retrieveMasterKey(
mainCredential.masterPassword,
keyFileInputStream
databaseKDB.deriveMasterKey(
contentResolver,
mainCredential
)
}
databaseKDB
setDatabaseKDB(databaseKDB)
},
{ databaseInputStream ->
val databaseKDBX = DatabaseKDBX().apply {
@@ -655,23 +606,23 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream,
progressTaskUpdater) {
databaseKDBX.retrieveMasterKey(
mainCredential.masterPassword,
keyFileInputStream,
databaseKDBX.deriveMasterKey(
contentResolver,
mainCredential,
challengeResponseRetriever
)
}
}
databaseKDBX
setDatabaseKDBX(databaseKDBX)
}
)
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
loaded = true
} catch (e: Exception) {
throw LoadDatabaseException(e)
Log.e(TAG, "Unable to load the database")
if (e is DatabaseInputException)
throw e
throw DatabaseInputException(e)
} finally {
keyFileInputStream?.close()
dataModifiedSinceLastLoading = false
}
}
@@ -680,48 +631,44 @@ class Database {
return mDatabaseKDBX != null
}
@Throws(LoadDatabaseException::class)
fun mergeData(databaseToMergeUri: Uri?,
databaseToMergeMainCredential: MainCredential?,
contentResolver: ContentResolver,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@Throws(DatabaseInputException::class)
fun mergeData(
contentResolver: ContentResolver,
databaseToMergeUri: Uri?,
databaseToMergeMainCredential: MainCredential?,
databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
mDatabaseKDB?.let {
throw IODatabaseException("Unable to merge from a database V1")
throw MergeDatabaseKDBException()
}
// New database instance to get new changes
val databaseToMerge = Database()
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try {
val databaseUri = databaseToMerge.fileUri
if (databaseUri != null) {
if (databaseToMergeMainCredential != null) {
// Get keyFile inputStream
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
}
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream ->
val databaseToMergeKDB = DatabaseKDB()
DatabaseInputKDB(databaseToMergeKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) {
databaseToMergeKDB.retrieveMasterKey(
databaseToMergeMainCredential.masterPassword,
keyFileInputStream,
databaseToMergeKDB.deriveMasterKey(
contentResolver,
databaseToMergeMainCredential
)
} else {
databaseToMergeKDB.masterKey = masterKey
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
}
}
databaseToMergeKDB
databaseToMerge.setDatabaseKDB(databaseToMergeKDB)
},
{ databaseInputStream ->
val databaseToMergeKDBX = DatabaseKDBX()
@@ -729,18 +676,22 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) {
databaseToMergeKDBX.retrieveMasterKey(
databaseToMergeMainCredential.masterPassword,
keyFileInputStream,
databaseToMergeKDBX.deriveMasterKey(
contentResolver,
databaseToMergeMainCredential,
databaseToMergeChallengeResponseRetriever
)
} else {
databaseToMergeKDBX.masterKey = masterKey
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
}
}
}
databaseToMergeKDBX
databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX)
}
)
loaded = true
mDatabaseKDBX?.let { currentDatabaseKDBX ->
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
@@ -760,24 +711,24 @@ class Database {
}
}
} else {
throw IODatabaseException("Database URI is null, database cannot be merged")
throw UnknownDatabaseLocationException()
}
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
Log.e(TAG, "Unable to merge the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally {
keyFileInputStream?.close()
databaseToMerge.clearAndClose()
}
}
@Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@Throws(DatabaseInputException::class)
fun reloadData(
contentResolver: ContentResolver,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
// Retrieve the stream from the old database URI
try {
@@ -791,9 +742,11 @@ class Database {
}
DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDB.masterKey = masterKey
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
}
databaseKDB
setDatabaseKDB(databaseKDB)
},
{ databaseInputStream ->
val databaseKDBX = DatabaseKDBX()
@@ -803,26 +756,144 @@ class Database {
DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDBX.masterKey = masterKey
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
}
}
databaseKDBX
setDatabaseKDBX(databaseKDBX)
}
)
loaded = true
} else {
throw IODatabaseException("Database URI is null, database cannot be reloaded")
throw UnknownDatabaseLocationException()
}
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
Log.e(TAG, "Unable to reload the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally {
dataModifiedSinceLastLoading = false
}
}
@Throws(Exception::class)
private fun readDatabaseStream(contentResolver: ContentResolver,
databaseUri: Uri,
openDatabaseKDB: (InputStream) -> Unit,
openDatabaseKDBX: (InputStream) -> Unit) {
try {
// Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, databaseUri)
?: throw UnknownDatabaseLocationException()
BufferedInputStream(databaseStream).use { databaseInputStream ->
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> openDatabaseKDB(
databaseInputStream
)
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> openDatabaseKDBX(
databaseInputStream
)
// Header not recognized
else -> throw SignatureDatabaseException()
}
}
} catch (fileNotFoundException : FileNotFoundException) {
throw FileNotFoundDatabaseException()
}
}
@Throws(DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver,
cacheDir: File,
databaseCopyUri: Uri?,
mainCredential: MainCredential?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) {
val saveUri = databaseCopyUri ?: this.fileUri
// Build temp database file to avoid file corruption if error
val cacheFile = File(cacheDir, saveUri.hashCode().toString())
try {
if (saveUri != null) {
// Save in a temp memory to avoid exception
cacheFile.outputStream().use { outputStream ->
mDatabaseKDB?.let { databaseKDB ->
DatabaseOutputKDB(databaseKDB).apply {
writeDatabase(outputStream) {
if (mainCredential != null) {
databaseKDB.deriveMasterKey(
contentResolver,
mainCredential
)
} else {
// No master key change
}
}
}
}
?: mDatabaseKDBX?.let { databaseKDBX ->
DatabaseOutputKDBX(databaseKDBX).apply {
writeDatabase(outputStream) {
if (mainCredential != null) {
// Build new master key from MainCredential
databaseKDBX.deriveMasterKey(
contentResolver,
mainCredential,
challengeResponseRetriever
)
} else {
// Reuse composite key parts
databaseKDBX.deriveCompositeKey(
challengeResponseRetriever
)
}
}
}
}
}
// Copy from the cache to the final stream
UriUtil.getUriOutputStream(contentResolver, saveUri)?.use { outputStream ->
cacheFile.inputStream().use { inputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
} else {
throw UnknownDatabaseLocationException()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to save database", e)
if (e is DatabaseException)
throw e
throw DatabaseOutputException(e)
} finally {
try {
Log.d(TAG, "Delete database cache file $cacheFile")
cacheFile.delete()
} catch (e: Exception) {
Log.e(TAG, "Cache file $cacheFile cannot be deleted", e)
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
}
}
fun groupIsInRecycleBin(group: Group): Boolean {
val groupKDB = group.groupKDB
val groupKDBX = group.groupKDBX
@@ -845,13 +916,13 @@ class Database {
fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
fromGroup: NodeId<*>? = null,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
return mSearchHelper.createVirtualGroupWithSearchResult(this,
searchParameters, fromGroup, max)
}
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
return mSearchHelper.createVirtualGroupWithSearchResult(this,
SearchParameters().apply {
searchQuery = searchInfoString
searchInTitles = true
@@ -908,40 +979,6 @@ class Database {
dataModifiedSinceLastLoading = true
}
@Throws(DatabaseOutputException::class)
fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) {
try {
val saveUri = databaseCopyUri ?: this.fileUri
if (saveUri != null) {
var outputStream: OutputStream? = null
try {
outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri)
outputStream?.let { definedOutputStream ->
val databaseOutput =
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let {
DatabaseOutputKDBX(
it,
definedOutputStream
)
}
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to save database", e)
throw DatabaseOutputException(e)
}
}
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
this.mDatabaseKDB?.clearIndexes()
this.mDatabaseKDBX?.clearIndexes()
@@ -987,20 +1024,13 @@ class Database {
}
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
val password = mainCredential.masterPassword
val password = mainCredential.password
val containsKeyFile = mainCredential.keyFileUri != null
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false
}
@Throws(IOException::class)
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.keyLastChanged = DateInstant()
}
fun rootCanContainsEntry(): Boolean {
return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false
}

View File

@@ -0,0 +1,276 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX 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.
*
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
data class MainCredential(var password: String? = null,
var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null): Parcelable {
constructor(parcel: Parcel) : this() {
password = parcel.readString()
keyFileUri = parcel.readParcelable(Uri::class.java.classLoader)
hardwareKey = parcel.readEnum<HardwareKey>()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(password)
parcel.writeParcelable(keyFileUri, flags)
parcel.writeEnum(hardwareKey)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MainCredential
if (password != other.password) return false
if (keyFileUri != other.keyFileUri) return false
if (hardwareKey != other.hardwareKey) return false
return true
}
override fun hashCode(): Int {
var result = password?.hashCode() ?: 0
result = 31 * result + (keyFileUri?.hashCode() ?: 0)
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
private val TAG = MainCredential::class.java.simpleName
@Throws(IOException::class)
fun retrievePasswordKey(key: String,
encoding: Charset
): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
}
@Throws(IOException::class)
fun retrieveFileKey(contentResolver: ContentResolver,
keyFileUri: Uri?,
allowXML: Boolean): ByteArray {
if (keyFileUri == null)
throw IOException("Keyfile URI is null")
val keyData = getKeyFileData(contentResolver, keyFileUri)
?: throw IOException("No data retrieved")
try {
// Check XML key file
val xmlKeyByteArray = if (allowXML)
loadXmlKeyFile(ByteArrayInputStream(keyData))
else
null
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (e: Exception) {
throw IOException("Unable to load the keyfile.", e)
}
}
@Throws(IOException::class)
fun retrieveHardwareKey(keyData: ByteArray): ByteArray {
return HashManager.hashSha256(keyData)
}
@Throws(Exception::class)
private fun getKeyFileData(contentResolver: ContentResolver,
keyFileUri: Uri): ByteArray? {
UriUtil.getUriInputStream(contentResolver, keyFileUri)?.use { keyFileInputStream ->
return keyFileInputStream.readBytes()
}
return null
}
private fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
if (keyFileChildNodes.length < 2)
return null
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
// <Meta>
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
for (metaChildPosition in 0 until metaChildNodes.length) {
val metaChildNode = metaChildNodes.item(metaChildPosition)
// <Version>
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
val versionChildNodes = metaChildNode.childNodes
for (versionChildPosition in 0 until versionChildNodes.length) {
val versionChildNode = versionChildNodes.item(versionChildPosition)
if (versionChildNode.nodeType == Node.TEXT_NODE) {
val versionText = versionChildNode.textContent.removeSpaceChars()
try {
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString,
DatabaseKDBX.BASE_64_FLAG
)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)
) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
}
}
}
}
}
}
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
var success = false
try {
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4).toHexString()
success = dataDigest == hash
} catch (e: Exception) {
e.printStackTrace()
}
return success
}
private const val XML_NODE_ROOT_NAME = "KeyFile"
private const val XML_NODE_META_NAME = "Meta"
private const val XML_NODE_VERSION_NAME = "Version"
private const val XML_NODE_KEY_NAME = "Key"
private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
}
}

View File

@@ -19,11 +19,13 @@
package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -31,8 +33,10 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.util.*
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
@@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
KdfFactory.aesKdf
)
override val passwordEncoding: String
get() = "ISO-8859-1"
override val passwordEncoding: Charset
get() = Charsets.ISO_8859_1
override var numberKeyEncryptionRounds = 300L
@@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return newId
}
@Throws(IOException::class)
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
return if (key != null && keyInputStream != null) {
getCompositeKey(key, keyInputStream)
} else if (key != null) { // key.length() >= 0
getPasswordKey(key)
} else if (keyInputStream != null) { // key == null
getFileKey(keyInputStream)
} else {
throw IllegalArgumentException("Key cannot be empty.")
}
}
@Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
// Encrypt the master key a few times to make brute-force key-search harder
@@ -138,6 +128,41 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
}
fun deriveMasterKey(
contentResolver: ContentResolver,
mainCredential: MainCredential
) {
// Exception when no password
if (mainCredential.hardwareKey != null)
throw HardwareKeyDatabaseException()
if (mainCredential.password == null && mainCredential.keyFileUri == null)
throw EmptyKeyDatabaseException()
// Retrieve plain data
val password = mainCredential.password
val keyFileUri = mainCredential.keyFileUri
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
password,
passwordEncoding
) else null
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
contentResolver,
keyFileUri,
false
) else null
// Build master key
if (passwordBytes != null
&& keyFileBytes != null) {
this.masterKey = HashManager.hashSha256(
passwordBytes,
keyFileBytes
)
} else {
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
}
}
override fun createGroup(): GroupKDB {
return GroupKDB()
}

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.content.res.Resources
import android.util.Base64
import android.util.Log
@@ -31,10 +32,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -49,30 +47,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.Mac
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// To resave the database with same credential when already loaded
private var mCompositeKey = CompositeKey()
var hmacKey: ByteArray? = null
private set
@@ -233,6 +228,79 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
}
fun deriveMasterKey(
contentResolver: ContentResolver,
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
) {
// Retrieve each plain credential
val password = mainCredential.password
val keyFileUri = mainCredential.keyFileUri
val hardwareKey = mainCredential.hardwareKey
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
password,
passwordEncoding
) else null
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
contentResolver,
keyFileUri,
true
) else null
val hardwareKeyBytes = if (hardwareKey != null) MainCredential.retrieveHardwareKey(
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
) else null
// Save to rebuild master password with new seed later
mCompositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKey)
// Build the master key
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes,
hardwareKeyBytes
)
}
@Throws(DatabaseOutputException::class)
fun deriveCompositeKey(
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
) {
val passwordBytes = mCompositeKey.passwordData
val keyFileBytes = mCompositeKey.keyFileData
val hardwareKey = mCompositeKey.hardwareKey
if (hardwareKey == null) {
// If no hardware key, simply rebuild from composed keys
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes
)
} else {
val hardwareKeyBytes = MainCredential.retrieveHardwareKey(
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
)
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes,
hardwareKeyBytes
)
}
}
private fun composedKeyToMasterKey(passwordData: ByteArray?,
keyFileData: ByteArray?,
hardwareKeyData: ByteArray? = null): ByteArray {
return HashManager.hashSha256(
passwordData,
keyFileData,
hardwareKeyData
)
}
fun copyMasterKeyFrom(databaseVersioned: DatabaseKDBX) {
super.copyMasterKeyFrom(databaseVersioned)
this.mCompositeKey = databaseVersioned.mCompositeKey
}
fun getMinKdbxVersion(): UnsignedInt {
val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler()
@@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
kdfEngine.setParallelism(kdfParameters!!, parallelism)
}
override val passwordEncoding: String
get() = "UTF-8"
override val passwordEncoding: Charset
get() = Charsets.UTF_8
private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? {
if (groupUUID == UUID_ZERO)
@@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return mFieldReferenceEngine.compile(textReference, recursionLevel)
}
@Throws(IOException::class)
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
var masterKey = byteArrayOf()
if (key != null && keyInputStream != null) {
return getCompositeKey(key, keyInputStream)
} else if (key != null) { // key.length() >= 0
masterKey = getPasswordKey(key)
} else if (keyInputStream != null) { // key == null
masterKey = getFileKey(keyInputStream)
}
return HashManager.hashSha256(masterKey)
}
@Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray) {
@@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return ret
}
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
if (keyFileChildNodes.length < 2)
return null
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
// <Meta>
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
for (metaChildPosition in 0 until metaChildNodes.length) {
val metaChildNode = metaChildNodes.item(metaChildPosition)
// <Version>
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
val versionChildNodes = metaChildNode.childNodes
for (versionChildPosition in 0 until versionChildNodes.length) {
val versionChildNode = versionChildNodes.item(versionChildPosition)
if (versionChildNode.nodeType == Node.TEXT_NODE) {
val versionText = versionChildNode.textContent.removeSpaceChars()
try {
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString, BASE_64_FLAG)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
}
}
}
}
}
}
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
var success = false
try {
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4).toHexString()
success = dataDigest == hash
} catch (e: Exception) {
e.printStackTrace()
}
return success
}
override fun newGroupId(): NodeIdUUID {
var newId: NodeIdUUID
do {
@@ -928,13 +871,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
private const val XML_NODE_ROOT_NAME = "KeyFile"
private const val XML_NODE_META_NAME = "Meta"
private const val XML_NODE_VERSION_NAME = "Version"
private const val XML_NODE_KEY_NAME = "Key"
private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP
}
}

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.element.database
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
@@ -32,11 +31,9 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import org.apache.commons.codec.binary.Hex
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.*
abstract class DatabaseVersioned<
@@ -46,7 +43,6 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> {
// Algorithm used to encrypt the database
abstract var encryptionAlgorithm: EncryptionAlgorithm
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
@@ -55,11 +51,12 @@ abstract class DatabaseVersioned<
abstract val kdfAvailableList: List<KdfEngine>
abstract var numberKeyEncryptionRounds: Long
protected abstract val passwordEncoding: String
abstract val passwordEncoding: Charset
var masterKey = ByteArray(32)
var finalKey: ByteArray? = null
protected set
var transformSeed: ByteArray? = null
abstract val version: String
abstract val defaultFileExtension: String
@@ -91,58 +88,6 @@ abstract class DatabaseVersioned<
return getGroupIndexes().filter { it != rootGroup }
}
@Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
masterKey = getMasterKey(key, keyfileInputStream)
}
@Throws(IOException::class)
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key)
return HashManager.hashSha256(passwordKey, fileKey)
}
@Throws(IOException::class)
protected fun getPasswordKey(key: String): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(charset(passwordEncoding))
} catch (e: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
}
@Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
try {
val keyData = keyInputStream.readBytes()
// Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (outOfMemoryError: OutOfMemoryError) {
throw IOException("Keyfile data is too large", outOfMemoryError)
}
}
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
@@ -158,20 +103,25 @@ abstract class DatabaseVersioned<
val bKey: ByteArray
try {
bKey = password.toByteArray(charset(encoding))
bKey = password.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
return false
}
val reEncoded: String
try {
reEncoded = String(bKey, charset(encoding))
reEncoded = String(bKey, encoding)
} catch (e: UnsupportedEncodingException) {
return false
}
return password == reEncoded
}
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
this.masterKey = databaseVersioned.masterKey
this.transformSeed = databaseVersioned.transformSeed
}
/*
* -------------------------------------
* Node Creation

View File

@@ -24,128 +24,172 @@ import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import java.io.PrintStream
import java.io.PrintWriter
abstract class DatabaseException : Exception {
var innerMessage: String? = null
abstract var errorId: Int
var parameters: (Array<out String>)? = null
var mThrowable: Throwable? = null
constructor() : super()
constructor(message: String) : super(message)
constructor(message: String, throwable: Throwable) : super(message, throwable)
constructor(throwable: Throwable) : super(throwable)
constructor(message: String, throwable: Throwable) {
mThrowable = throwable
innerMessage = StringBuilder().apply {
append(message)
if (throwable.localizedMessage != null) {
append(" ")
append(throwable.localizedMessage)
}
}.toString()
}
constructor(throwable: Throwable) {
mThrowable = throwable
innerMessage = throwable.localizedMessage
}
fun getLocalizedMessage(resources: Resources): String {
parameters?.let {
return resources.getString(errorId, *it)
} ?: return resources.getString(errorId)
val throwable = mThrowable
if (throwable is DatabaseException)
errorId = throwable.errorId
val localMessage = parameters?.let {
resources.getString(errorId, *it)
} ?: resources.getString(errorId)
return StringBuilder().apply {
append(localMessage)
if (innerMessage != null) {
append(" ")
append(innerMessage)
}
}.toString()
}
override fun printStackTrace() {
mThrowable?.printStackTrace()
super.printStackTrace()
}
override fun printStackTrace(s: PrintStream) {
mThrowable?.printStackTrace(s)
super.printStackTrace(s)
}
override fun printStackTrace(s: PrintWriter) {
mThrowable?.printStackTrace(s)
super.printStackTrace(s)
}
}
open class LoadDatabaseException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable)
}
class FileNotFoundDatabaseException : LoadDatabaseException {
class FileNotFoundDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.file_not_found_content
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
}
class InvalidAlgorithmDatabaseException : LoadDatabaseException {
class CorruptedDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.corrupted_file
}
class InvalidAlgorithmDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.invalid_algorithm
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class DuplicateUuidDatabaseException: LoadDatabaseException {
class UnknownDatabaseLocationException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.invalid_db_same_uuid
constructor(type: Type, uuid: NodeId<*>) : super() {
parameters = arrayOf(type.name, uuid.toString())
}
constructor(exception: Throwable) : super(exception)
override var errorId: Int = R.string.error_location_unknown
}
class IODatabaseException : LoadDatabaseException {
class HardwareKeyDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_hardware_key_unsupported
}
class EmptyKeyDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_empty_key
}
class SignatureDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.invalid_db_sig
}
class VersionDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.unsupported_db_version
}
class InvalidCredentialsDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.invalid_credentials
constructor() : super()
constructor(string: String) : super(string)
}
class KDFMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
@StringRes
override var errorId: Int = R.string.error_load_database_KDF_memory
}
class NoMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
@StringRes
override var errorId: Int = R.string.error_out_of_memory
}
class DuplicateUuidDatabaseException(type: Type, uuid: NodeId<*>) : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.invalid_db_same_uuid
init {
parameters = arrayOf(type.name, uuid.toString())
}
}
class XMLMalformedDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.error_XML_malformed
constructor() : super()
constructor(string: String) : super(string)
}
class MergeDatabaseKDBException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.error_unable_merge_database_kdb
}
class MoveEntryDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
}
class MoveGroupDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_move_group_here
}
class CopyEntryDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_copy_entry_here
}
class CopyGroupDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_copy_group_here
}
open class DatabaseInputException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
constructor(throwable: Throwable) : super(throwable)
}
class KDFMemoryDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database_KDF_memory
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class SignatureDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_db_sig
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class VersionDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.unsupported_db_version
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class InvalidCredentialsDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_credentials
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class NoMemoryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_out_of_memory
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class MoveEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class MoveGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class CopyEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_copy_entry_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class CopyGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_copy_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
// TODO Output Exception
open class DatabaseOutputException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_save_database

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.InputStream
@@ -33,15 +33,9 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var m
/**
* Load a versioned database file, return contents in a new DatabaseVersioned.
*
* @param databaseInputStream Existing file to load.
* @param password Pass phrase for infile.
* @return new DatabaseVersioned container.
*
* @throws LoadDatabaseException on database error (contains IO exceptions)
*/
@Throws(LoadDatabaseException::class)
@Throws(DatabaseInputException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): D

View File

@@ -47,7 +47,7 @@ import javax.crypto.CipherInputStream
class DatabaseInputKDB(database: DatabaseKDB)
: DatabaseInput<DatabaseKDB>(database) {
@Throws(LoadDatabaseException::class)
@Throws(DatabaseInputException::class)
override fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): DatabaseKDB {
@@ -76,6 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
throw VersionDatabaseException()
}
mDatabase.transformSeed = header.transformSeed
assignMasterKey.invoke()
// Select algorithm
@@ -310,18 +311,11 @@ class DatabaseInputKDB(database: DatabaseKDB)
stopContentTimer()
} catch (e: LoadDatabaseException) {
} catch (e: Error) {
mDatabase.clearAll()
throw e
} catch (e: IOException) {
mDatabase.clearAll()
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
mDatabase.clearAll()
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
mDatabase.clearAll()
throw LoadDatabaseException(e)
if (e is OutOfMemoryError)
throw NoMemoryDatabaseException(e)
throw DatabaseInputException(e)
}
return mDatabase

View File

@@ -99,7 +99,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
this.isRAMSufficient = method
}
@Throws(LoadDatabaseException::class)
@Throws(DatabaseInputException::class)
override fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): DatabaseKDBX {
@@ -114,6 +114,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header
val transformSeed = header.transformSeed
mDatabase.transformSeed = transformSeed
assignMasterKey.invoke()
mDatabase.makeFinalKey(header.masterSeed)
@@ -155,7 +157,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
throw InvalidCredentialsDatabaseException()
}
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException()
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
@@ -187,7 +189,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
try {
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
} catch (e: Exception) {
throw LoadDatabaseException(e)
throw DatabaseInputException(e)
}
val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
@@ -200,19 +202,12 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
stopContentTimer()
} catch (e: LoadDatabaseException) {
throw e
} catch (e: XmlPullParserException) {
throw IODatabaseException(e)
} catch (e: IOException) {
} catch (e: Error) {
if (e is OutOfMemoryError)
throw NoMemoryDatabaseException(e)
if (e.message?.contains("Hash failed with code") == true)
throw KDFMemoryDatabaseException(e)
else
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
throw LoadDatabaseException(e)
throw DatabaseInputException(e)
}
return mDatabase
@@ -227,7 +222,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
val fieldId = dataInputStream.read().toByte()
val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
if (size < 0) throw IOException("Corrupted file")
if (size < 0) throw CorruptedDatabaseException()
var data = ByteArray(0)
try {
@@ -238,7 +233,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
}
} catch (e: Exception) {
// OOM only if corrupted file
throw IOException("Corrupted file")
throw CorruptedDatabaseException()
}
readStream = true
@@ -297,7 +292,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
Binaries
}
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
private fun readDocumentStreamed(xpp: XmlPullParser) {
ctxGroups.clear()
@@ -324,11 +319,11 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
}
// Error checks
if (ctx != KdbContext.Null) throw IOException("Malformed")
if (ctxGroups.size != 0) throw IOException("Malformed")
if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException()
if (ctxGroups.size != 0) throw XMLMalformedDatabaseException()
}
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
val name = xpp.name
when (ctx) {
@@ -352,7 +347,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
if (encodedHash.isNotEmpty() && hashOfHeader != null) {
val hash = Base64.decode(encodedHash, BASE_64_FLAG)
if (!Arrays.equals(hash, hashOfHeader)) {
throw LoadDatabaseException()
throw DatabaseInputException()
}
}
} else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
@@ -824,7 +819,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
if (ctx != null) {
contextName = ctx.name
}
throw RuntimeException("Invalid end element: Context " + contextName + "End element: " + name)
throw XMLMalformedDatabaseException("Invalid end element: Context " + contextName + "End element: " + name)
}
}

View File

@@ -26,7 +26,7 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
abstract class DatabaseOutput<Header : DatabaseHeader> {
@Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom {
@@ -44,9 +44,7 @@ abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(pro
}
@Throws(DatabaseOutputException::class)
abstract fun output()
@Throws(DatabaseOutputException::class)
abstract fun outputHeader(outputStream: OutputStream): Header
abstract fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit)
}

View File

@@ -39,9 +39,8 @@ import java.security.*
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
outputStream: OutputStream)
: DatabaseOutput<DatabaseHeaderKDB>(outputStream) {
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB)
: DatabaseOutput<DatabaseHeaderKDB>() {
private var headerHashBlock: ByteArray? = null
@@ -60,15 +59,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
@Throws(DatabaseOutputException::class)
override fun output() {
override fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit) {
// 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
// also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup
sortNodesForOutput()
val header = outputHeader(mOutputStream)
val header = outputHeader(outputStream, assignMasterKey)
val finalKey = getFinalKey(header)
val cipher: Cipher = try {
@@ -81,7 +80,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
try {
val cos = CipherOutputStream(mOutputStream, cipher)
val cos = CipherOutputStream(outputStream, cipher)
val bos = BufferedOutputStream(cos)
outputPlanGroupAndEntries(bos)
bos.flush()
@@ -107,7 +106,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
@Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
private fun outputHeader(outputStream: OutputStream,
assignMasterKey: () -> Unit): DatabaseHeaderKDB {
// Build header
val header = DatabaseHeaderKDB()
header.signature1 = DatabaseHeaderKDB.DBSIG_1
@@ -132,6 +132,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
setIVs(header)
mDatabaseKDB.transformSeed = header.transformSeed
assignMasterKey()
// Header checksum
val headerDigest: MessageDigest = HashManager.getHash256()

View File

@@ -56,9 +56,8 @@ import javax.crypto.CipherOutputStream
import kotlin.experimental.or
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
outputStream: OutputStream)
: DatabaseOutput<DatabaseHeaderKDBX>(outputStream) {
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
: DatabaseOutput<DatabaseHeaderKDBX>() {
private var randomStream: StreamCipher? = null
private lateinit var xml: XmlSerializer
@@ -67,43 +66,34 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private var headerHmac: ByteArray? = null
@Throws(DatabaseOutputException::class)
override fun output() {
override fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit) {
try {
header = outputHeader(mOutputStream)
header = outputHeader(outputStream, assignMasterKey)
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
val cos = attachStreamEncryptor(header!!, mOutputStream)
val cos = attachStreamEncryptor(header!!, outputStream)
cos.write(header!!.streamStartBytes)
HashedBlockOutputStream(cos)
} else {
mOutputStream.write(hashOfHeader!!)
mOutputStream.write(headerHmac!!)
outputStream.write(hashOfHeader!!)
outputStream.write(headerHmac!!)
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!))
}
val xmlOutputStream: OutputStream
try {
xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) {
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
else -> osPlain
}
when(mDatabaseKDBX.compressionAlgorithm) {
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
else -> osPlain
}.use { xmlOutputStream ->
if (!header!!.version.isBefore(FILE_VERSION_40)) {
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
}
outputDatabase(xmlOutputStream)
xmlOutputStream.close()
} catch (e: IllegalArgumentException) {
throw DatabaseOutputException(e)
} catch (e: IllegalStateException) {
throw DatabaseOutputException(e)
}
} catch (e: IOException) {
} catch (e: Exception) {
throw DatabaseOutputException(e)
}
}
@@ -322,11 +312,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX {
private fun outputHeader(outputStream: OutputStream,
assignMasterKey: () -> Unit): DatabaseHeaderKDBX {
try {
val header = DatabaseHeaderKDBX(mDatabaseKDBX)
setIVs(header)
mDatabaseKDBX.transformSeed = header.transformSeed
assignMasterKey.invoke()
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
pho.output()

View File

@@ -0,0 +1,35 @@
package com.kunzisoft.keepass.hardware
enum class HardwareKey(val value: String) {
FIDO2_SECRET("FIDO2 secret"),
CHALLENGE_RESPONSE_YUBIKEY("Yubikey challenge-response");
override fun toString(): String {
return value
}
companion object {
val DEFAULT = FIDO2_SECRET
fun getStringValues(): List<String> {
return values().map { it.value }
}
fun fromPosition(position: Int): HardwareKey {
return when (position) {
0 -> FIDO2_SECRET
1 -> CHALLENGE_RESPONSE_YUBIKEY
else -> DEFAULT
}
}
fun getHardwareKeyFromString(text: String?): HardwareKey? {
if (text == null)
return null
values().find { it.value == text }?.let {
return it
}
return DEFAULT
}
}
}

View File

@@ -0,0 +1,141 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.launch
class HardwareKeyResponseHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
private var getChallengeResponseResultLauncher: ActivityResultLauncher<Intent>? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
extra: Bundle?) -> Unit) {
val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
onChallengeResponded.invoke(challengeResponse,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} else {
Log.e(TAG, "Response from challenge error")
onChallengeResponded.invoke(null,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
}
}
getChallengeResponseResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
} else {
activity?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
}
}
fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
getChallengeResponseResultLauncher!!.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
}
}
companion object {
private val TAG = HardwareKeyResponseHelper::class.java.simpleName
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
fun isHardwareKeyAvailable(
activity: FragmentActivity,
hardwareKey: HardwareKey,
showDialog: Boolean = true
): Boolean {
return when (hardwareKey) {
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(activity.packageManager) != null
if (showDialog && !yubikeyDriverAvailable)
showHardwareKeyDriverNeeded(activity, hardwareKey)
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
activity: FragmentActivity,
hardwareKey: HardwareKey
) {
activity.lifecycleScope.launch {
val builder = AlertDialog.Builder(activity)
builder
.setMessage(
activity.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
UriUtil.openExternalApp(activity, activity.getString(R.string.key_driver_app_id))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
}
}

View File

@@ -41,11 +41,6 @@ class CipherEncryptDatabase(): Parcelable {
parcel.readByteArray(specParameters)
}
fun replaceContent(copy: CipherEncryptDatabase) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage)

View File

@@ -1,9 +1,11 @@
package com.kunzisoft.keepass.model
import android.net.Uri
import com.kunzisoft.keepass.hardware.HardwareKey
data class DatabaseFile(var databaseUri: Uri? = null,
var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null,
var databaseDecodedPath: String? = null,
var databaseAlias: String? = null,
var databaseFileExists: Boolean = false,

View File

@@ -1,32 +0,0 @@
package com.kunzisoft.keepass.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readParcelable(Uri::class.java.classLoader)) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(masterPassword)
parcel.writeParcelable(keyFileUri, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,13 @@
package com.kunzisoft.keepass.model
import androidx.annotation.StringRes
data class ProgressMessage(
@StringRes
var titleId: Int,
@StringRes
var messageId: Int? = null,
@StringRes
var warningId: Int? = null,
var cancelable: (() -> Unit)? = null
)

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX 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.
*
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.model
import android.content.Context

View File

@@ -27,6 +27,7 @@ import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.annotation.StringRes
import androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
@@ -37,12 +38,14 @@ import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -53,6 +56,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.*
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
@@ -61,20 +65,25 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mDatabase: Database? = null
// File description
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
private val mainScope = CoroutineScope(Dispatchers.Main)
private var mDatabaseListeners = LinkedList<DatabaseListener>()
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
private var mDatabaseListeners = mutableListOf<DatabaseListener>()
private var mDatabaseInfoListeners = mutableListOf<DatabaseInfoListener>()
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
// Channel to connect asynchronously a listener or a response
private var mRequestChallengeListenerChannel: Channel<RequestChallengeListener>? = null
private var mResponseChallengeChannel: Channel<ByteArray?>? = null
private var mActionRunning = false
private var mTaskRemovedRequested = false
private var mCreationState = false
private var mSaveState = false
private var mIconId: Int = R.drawable.notification_ic_database_load
private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null
private var mWarningId: Int? = null
private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened)
override fun retrieveChannelId(): String {
return CHANNEL_DATABASE_ID
@@ -114,6 +123,25 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.remove(actionTaskListener)
}
fun addRequestChallengeListener(requestChallengeListener: RequestChallengeListener) {
mainScope.launch {
val requestChannel = mRequestChallengeListenerChannel
if (requestChannel == null || requestChannel.isEmpty) {
initializeChallengeResponse()
mRequestChallengeListenerChannel?.send(requestChallengeListener)
} else {
cancelChallengeResponse(R.string.error_challenge_already_requested)
}
}
}
fun removeRequestChallengeListener() {
mainScope.launch {
mRequestChallengeListenerChannel?.cancel()
mRequestChallengeListenerChannel = null
}
}
}
interface DatabaseListener {
@@ -126,9 +154,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
interface ActionTaskListener {
fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result)
fun onStartAction(database: Database,
progressMessage: ProgressMessage)
fun onUpdateAction(database: Database,
progressMessage: ProgressMessage)
fun onStopAction(database: Database,
actionTask: String,
result: ActionRunnable.Result)
}
interface RequestChallengeListener {
fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?)
}
fun checkDatabase() {
@@ -165,7 +201,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info
if (!mCreationState && previousDatabaseInfo != null) {
if (!mSaveState && previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
}
@@ -197,12 +233,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mDatabase?.let { database ->
if (mActionRunning) {
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
actionTaskListener.onStartAction(
database, mProgressMessage
)
}
}
}
}
private fun initializeChallengeResponse() {
// Init the channels
if (mRequestChallengeListenerChannel == null) {
mRequestChallengeListenerChannel = Channel(0)
}
if (mResponseChallengeChannel == null) {
mResponseChallengeChannel = Channel(0)
}
}
private fun closeChallengeResponse() {
mRequestChallengeListenerChannel?.close()
mResponseChallengeChannel?.close()
mRequestChallengeListenerChannel = null
mResponseChallengeChannel = null
}
private fun cancelChallengeResponse(@StringRes error: Int) {
mRequestChallengeListenerChannel?.cancel(CancellationException(getString(error)))
mRequestChallengeListenerChannel = null
mResponseChallengeChannel?.cancel(CancellationException(getString(error)))
mResponseChallengeChannel = null
}
fun respondToChallenge(response: ByteArray) {
mainScope.launch {
val responseChannel = mResponseChallengeChannel
if (responseChannel == null || responseChannel.isEmpty) {
if (response.isEmpty()) {
cancelChallengeResponse(R.string.error_no_response_from_challenge)
} else {
mResponseChallengeChannel?.send(response)
}
} else {
cancelChallengeResponse(R.string.error_response_already_provided)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
@@ -219,8 +296,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
// Get save state
mSaveState = if (intent != null) {
if (intent.hasExtra(SAVE_DATABASE_KEY)) {
!database.isReadOnly && intent.getBooleanExtra(
SAVE_DATABASE_KEY,
mSaveState
)
} else (intent.action == ACTION_DATABASE_CREATE_TASK
|| intent.action == ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|| intent.action == ACTION_DATABASE_SAVE)
} else false
// Create the notification
buildMessage(intent, database.isReadOnly)
buildNotification(intent)
val intentAction = intent?.action
@@ -272,13 +361,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mActionRunning = true
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId)
putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId)
putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId)
})
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
actionTaskListener.onStartAction(
database, mProgressMessage
)
}
},
@@ -353,61 +444,51 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
private fun buildMessage(intent: Intent?, readOnly: Boolean) {
private fun buildNotification(intent: Intent?) {
// Assign elements for updates
val intentAction = intent?.action
var saveAction = false
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
saveAction = !readOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
}
mIconId = if (intentAction == null)
// Get icon depending action state
val iconId = if (intentAction == null)
R.drawable.notification_ic_database_open
else
R.drawable.notification_ic_database_load
R.drawable.notification_ic_database_action
mTitleId = when {
saveAction -> {
R.string.saving_database
}
intentAction == null -> {
// Title depending on action
mProgressMessage.titleId =
if (intentAction == null) {
R.string.database_opened
}
else -> {
when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
} else when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_ASSIGN_PASSWORD_TASK,
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
if (mSaveState)
R.string.saving_database
else
R.string.command_execution
}
}
}
}
mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> null
else -> null
}
// Updated later
mProgressMessage.messageId = null
mWarningId =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK
|| intentAction == ACTION_DATABASE_MERGE_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
null
else
// Warning if data is saved
mProgressMessage.warningId =
if (mSaveState)
R.string.do_not_kill_app
else
null
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(mIconId)
setSmallIcon(iconId)
intent?.let {
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
setContentTitle(getString(
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
)
}
setAutoCancel(false)
setContentIntent(null)
@@ -513,15 +594,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
override fun updateMessage(resId: Int) {
mMessageId = resId
private fun notifyProgressMessage() {
mDatabase?.let { database ->
mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onUpdateAction(database, mTitleId, mMessageId, mWarningId)
actionTaskListener.onUpdateAction(
database, mProgressMessage
)
}
}
}
override fun updateMessage(resId: Int) {
mProgressMessage.messageId = resId
notifyProgressMessage()
}
override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase)
@@ -539,6 +626,39 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
super.onTaskRemoved(rootIntent)
}
private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey,
seed: ByteArray?): ByteArray {
// Request a challenge - response
var response: ByteArray
runBlocking {
// Initialize the channels
initializeChallengeResponse()
val previousMessage = mProgressMessage.copy()
mProgressMessage.apply {
messageId = R.string.waiting_challenge_request
cancelable = {
cancelChallengeResponse(R.string.error_cancel_by_user)
}
}
// Send the request
notifyProgressMessage()
val challengeResponseRequestListener = mRequestChallengeListenerChannel?.receive()
challengeResponseRequestListener?.onChallengeResponseRequested(hardwareKey, seed)
// Wait the response
mProgressMessage.apply {
messageId = R.string.waiting_challenge_response
}
notifyProgressMessage()
response = mResponseChallengeChannel?.receive() ?: byteArrayOf()
// Close channels
closeChallengeResponse()
// Restore previous message
mProgressMessage = previousMessage
notifyProgressMessage()
}
return response
}
private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
@@ -550,15 +670,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (databaseUri == null)
return null
mCreationState = true
return CreateDatabaseRunnable(this,
database,
databaseUri,
getString(R.string.database_default_name),
getString(R.string.database),
getString(R.string.template_group_name),
mainCredential
mainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
) { result ->
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
@@ -586,17 +707,18 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (databaseUri == null)
return null
mCreationState = false
return LoadDatabaseRunnable(
this,
database,
databaseUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this
this,
database,
databaseUri,
mainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
readOnly,
cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this
) { result ->
// Add each info to reload database after thrown duplicate UUID exception
result.data = Bundle().apply {
@@ -626,6 +748,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
databaseToMergeUri,
databaseToMergeMainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
this
) { result ->
// No need to add each info to reload database
@@ -653,7 +778,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
databaseUri,
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} else {
null
}
@@ -687,7 +814,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
newGroup,
parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -712,7 +842,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldGroup,
newGroup,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -737,7 +870,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
newEntry,
parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -762,7 +898,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldEntry,
newEntry,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -783,7 +922,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
getListNodesFromBundle(database, intent.extras!!),
newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -804,7 +946,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
getListNodesFromBundle(database, intent.extras!!),
newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -820,7 +965,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
getListNodesFromBundle(database, intent.extras!!),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable())
AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} else {
null
}
@@ -838,7 +986,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -857,7 +1008,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database,
mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
}
} else {
null
@@ -881,7 +1035,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldElement,
newElement,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
}
@@ -897,7 +1053,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return RemoveUnlinkedDataDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply {
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
}
@@ -911,7 +1069,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
return SaveDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
).apply {
mAfterSaveDatabase = { result ->
result.data = intent.extras
@@ -936,6 +1098,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
SaveDatabaseRunnable(this,
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
databaseCopyUri)
} else {
null
@@ -1002,9 +1168,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
val nodesAction = ArrayList<Node>()
bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {

View File

@@ -96,6 +96,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
}
fun rememberHardwareKey(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.remember_hardware_key_key),
context.resources.getBoolean(R.bool.remember_hardware_key_default))
}
fun automaticallyFocusSearch(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded

View File

@@ -24,15 +24,18 @@ import android.app.Dialog
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import java.lang.Exception
import kotlinx.coroutines.launch
open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
open class ProgressTaskDialogFragment : DialogFragment() {
@StringRes
private var title = UNDEFINED
@@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
private var message = UNDEFINED
@StringRes
private var warning = UNDEFINED
private var cancellable: (() -> Unit)? = null
private var titleView: TextView? = null
private var messageView: TextView? = null
private var warningView: TextView? = null
private var cancelButton: Button? = null
private var progressView: ProgressBar? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
titleView = root.findViewById(R.id.progress_dialog_title)
messageView = root.findViewById(R.id.progress_dialog_message)
warningView = root.findViewById(R.id.progress_dialog_warning)
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
progressView = root.findViewById(R.id.progress_dialog_bar)
updateTitle(title)
updateMessage(message)
updateWarning(warning)
setCancellable(cancellable)
isCancelable = false
@@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
}
private fun updateView(textView: TextView?, @StringRes resId: Int) {
activity?.runOnUiThread {
activity?.lifecycleScope?.launch {
if (resId == UNDEFINED) {
textView?.visibility = View.GONE
} else {
@@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
}
}
fun updateTitle(@StringRes resId: Int) {
this.title = resId
private fun updateCancelable() {
activity?.lifecycleScope?.launch {
cancelButton?.isVisible = cancellable != null
cancelButton?.setOnClickListener {
cancellable?.invoke()
}
}
}
fun updateTitle(@StringRes resId: Int?) {
this.title = resId ?: UNDEFINED
updateView(titleView, title)
}
override fun updateMessage(@StringRes resId: Int) {
this.message = resId
fun updateMessage(@StringRes resId: Int?) {
this.message = resId ?: UNDEFINED
updateView(messageView, message)
}
fun updateWarning(@StringRes resId: Int) {
this.warning = resId
fun updateWarning(@StringRes resId: Int?) {
this.warning = resId ?: UNDEFINED
updateView(warningView, warning)
}
fun setCancellable(cancellable: (() -> Unit)?) {
this.cancellable = cancellable
updateCancelable()
}
companion object {
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"

View File

@@ -126,6 +126,26 @@ object ParcelableUtil {
}
}
fun Parcel.readByteArrayCompat(): ByteArray? {
val dataLength = readInt()
return if (dataLength >= 0) {
val data = ByteArray(dataLength)
readByteArray(data)
data
} else {
null
}
}
fun Parcel.writeByteArrayCompat(data: ByteArray?) {
if (data != null) {
writeInt(data.size)
writeByteArray(data)
} else {
writeInt(-1)
}
}
inline fun <reified T : Enum<T>> Parcel.readEnum() =
readString()?.let { enumValueOf<T>(it) }

View File

@@ -226,10 +226,10 @@ object UriUtil {
}
}
fun getUriFromIntent(intent: Intent, key: String): Uri? {
fun getUriFromIntent(intent: Intent?, key: String): Uri? {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
val clipData = intent.clipData
val clipData = intent?.clipData
if (clipData != null) {
if (clipData.description.label == key) {
if (clipData.itemCount == 1) {
@@ -242,7 +242,7 @@ object UriUtil {
}
}
} catch (e: Exception) {
return intent.getParcelableExtra(key)
return intent?.getParcelableExtra(key)
}
return null
}
@@ -269,11 +269,15 @@ object UriUtil {
fun contributingUser(context: Context): Boolean {
return (Education.isEducationScreenReclickedPerformed(context)
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro", false)
|| isExternalAppInstalled(
context,
context.getString(R.string.keepro_app_id),
false
)
)
}
private fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context)
@@ -295,10 +299,11 @@ object UriUtil {
}
try {
if (launchIntent == null) {
// TODO F-Droid
context.startActivity(
Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))
.setData(Uri.parse(context.getString(R.string.play_store_url, packageName)))
)
} else {
context.startActivity(launchIntent)

View File

@@ -0,0 +1,149 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.text.InputType
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: ConstraintLayout(context, attrs, defStyle) {
private var mHardwareKey: HardwareKey? = null
private val hardwareKeyLayout: TextInputLayout
private val hardwareKeyCompletion: AppCompatAutoCompleteTextView
var selectionListener: ((HardwareKey)-> Unit)? = null
private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context)
private class ArrayAdapterNoFilter(context: Context)
: ArrayAdapter<String>(context, android.R.layout.simple_list_item_1) {
val hardwareKeys = HardwareKey.values()
override fun getCount(): Int {
return hardwareKeys.size
}
override fun getItem(position: Int): String {
return hardwareKeys[position].value
}
override fun getItemId(position: Int): Long {
// Or just return p0
return hardwareKeys[position].hashCode().toLong()
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(p0: CharSequence?): FilterResults {
return FilterResults().apply {
values = hardwareKeys
}
}
override fun publishResults(p0: CharSequence?, p1: FilterResults?) {
notifyDataSetChanged()
}
}
}
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_hardware_key_selection, this)
hardwareKeyLayout = findViewById(R.id.input_entry_hardware_key_layout)
hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion)
hardwareKeyCompletion.isFocusable = false
hardwareKeyCompletion.isFocusableInTouchMode = false
//hardwareKeyCompletion.isEnabled = false
hardwareKeyCompletion.isCursorVisible = false
hardwareKeyCompletion.setTextIsSelectable(false)
hardwareKeyCompletion.inputType = InputType.TYPE_NULL
hardwareKeyCompletion.setAdapter(mHardwareKeyAdapter)
hardwareKeyCompletion.setOnClickListener {
hardwareKeyCompletion.showDropDown()
}
hardwareKeyCompletion.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
mHardwareKey = HardwareKey.fromPosition(position)
mHardwareKey?.let { hardwareKey ->
selectionListener?.invoke(hardwareKey)
}
}
}
var hardwareKey: HardwareKey?
get() {
return mHardwareKey
}
set(value) {
mHardwareKey = value
hardwareKeyCompletion.setText(value?.toString() ?: "")
}
var error: CharSequence?
get() = hardwareKeyLayout.error
set(value) {
hardwareKeyLayout.error = value
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)
saveState.mHardwareKey = this.mHardwareKey
return saveState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mHardwareKey = state.mHardwareKey
}
internal class SavedState : BaseSavedState {
var mHardwareKey: HardwareKey? = null
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
mHardwareKey = parcel.readEnum<HardwareKey>()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeEnum(mHardwareKey)
}
companion object CREATOR : Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -39,19 +39,24 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var passwordTextView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton
private var passwordTextView: EditText
private var checkboxKeyFileView: CompoundButton
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxHardwareView: CompoundButton
private var hardwareKeySelectionView: HardwareKeySelectionView
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onValidateListener: (() -> Unit)? = null
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
@@ -60,15 +65,17 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this)
passwordTextView = findViewById(R.id.password_text_view)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
passwordTextView = findViewById(R.id.password_text_view)
checkboxKeyFileView = findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxHardwareView = findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = findViewById(R.id.hardware_key_selection)
val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onValidateListener?.invoke()
validateCredential()
return true
}
return false
@@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
) {
onValidateListener?.invoke()
validateCredential()
handled = true
}
handled
@@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
onPasswordChecked?.onCheckedChanged(view, checked)
}
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (keyFileSelectionView.uri == null) {
checkboxKeyFileView.isChecked = false
}
}
onKeyFileChecked?.onCheckedChanged(view, checked)
}
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (hardwareKeySelectionView.hardwareKey == null) {
checkboxHardwareView.isChecked = false
}
}
onHardwareKeyChecked?.onCheckedChanged(view, checked)
}
hardwareKeySelectionView.selectionListener = { _ ->
checkboxHardwareView.isChecked = true
}
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
fun validateCredential() {
onValidateListener?.invoke()
}
fun populatePasswordTextView(text: String?) {
@@ -118,7 +145,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
}
fun populateKeyFileTextView(uri: Uri?) {
fun populateKeyFileView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView.uri = null
if (checkboxKeyFileView.isChecked)
@@ -130,16 +157,36 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
}
fun populateHardwareKeyView(hardwareKey: HardwareKey?) {
if (hardwareKey == null) {
hardwareKeySelectionView.hardwareKey = null
if (checkboxHardwareView.isChecked)
checkboxHardwareView.isChecked = false
} else {
hardwareKeySelectionView.hardwareKey = hardwareKey
if (!checkboxHardwareView.isChecked)
checkboxHardwareView.isChecked = true
}
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
}
fun isFill(): Boolean {
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked
return checkboxPasswordView.isChecked
|| (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|| (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
}
fun getMainCredential(): MainCredential {
return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked)
this.password = if (checkboxPasswordView.isChecked)
passwordTextView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null
this.hardwareKey = if (checkboxHardwareView.isChecked)
hardwareKeySelectionView.hardwareKey else null
}
}
@@ -151,7 +198,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
// TODO HARDWARE_KEY
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> false
CredentialStorage.HARDWARE_KEY -> false
}
}

View File

@@ -0,0 +1,25 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ChallengeResponseViewModel: ViewModel() {
val dataResponded : LiveData<ByteArray?> get() = _dataResponded
private val _dataResponded = MutableLiveData<ByteArray?>()
fun respond(byteArray: ByteArray) {
_dataResponded.value = byteArray
}
fun resendResponse() {
dataResponded.value?.let {
_dataResponded.value = it
}
}
fun consumeResponse() {
_dataResponded.value = null
}
}

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
@@ -72,8 +73,12 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
}
}
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded ->
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?, hardwareKey: HardwareKey?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(
databaseUri,
keyFileUri,
hardwareKey
) { databaseFileAdded ->
databaseFileAdded?.let { _ ->
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
this.databaseFileAction = DatabaseFileAction.ADD
@@ -96,6 +101,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
.find { it.databaseUri == databaseFileUpdated.databaseUri }
?.apply {
keyFileUri = databaseFileUpdated.keyFileUri
hardwareKey = databaseFileUpdated.hardwareKey
databaseAlias = databaseFileUpdated.databaseAlias
databaseFileExists = databaseFileUpdated.databaseFileExists
databaseLastModified = databaseFileUpdated.databaseLastModified

View File

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 897 B

View File

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 657 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -58,7 +58,7 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="144dp"
android:minHeight="106dp"
android:layout_marginTop="?attr/actionBarSize"
android:background="?attr/colorPrimary">
<LinearLayout

View File

@@ -58,6 +58,16 @@
android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.Warning"/>
<Button
android:id="@+id/progress_dialog_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:text="@string/entry_cancel" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_dialog_bar"
app:indicatorColor="?attr/colorAccent"

View File

@@ -20,6 +20,7 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:filterTouchesWhenObscured="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
@@ -115,7 +116,7 @@
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:id="@+id/keyfile_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_keyfile"/>
@@ -126,9 +127,41 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/keyfile_checkox"
app:layout_constraintStart_toEndOf="@+id/keyfile_checkbox"
app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_view_hardware_key"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hardware_key"/>
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/hardware_key_checkbox"
app:layout_constraintEnd_toEndOf="parent"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_hardware_key"
android:layout_marginBottom="@dimen/default_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
android:importantForAccessibility="no"
tools:ignore="UnusedAttribute">
<com.google.android.material.textfield.TextInputLayout
style="@style/KeepassDXStyle.TextInputLayout.ExposedMenu"
android:id="@+id/input_entry_hardware_key_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hardware_key">
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
android:id="@+id/input_entry_hardware_key_completion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>

View File

@@ -63,7 +63,7 @@
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox"
android:id="@+id/keyfile_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection"
@@ -76,9 +76,35 @@
android:id="@+id/keyfile_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_toRightOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkox"
android:layout_toEndOf="@+id/keyfile_checkbox"
android:layout_toRightOf="@+id/keyfile_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:minHeight="48dp" />
</RelativeLayout>
<!-- Hardware key -->
<RelativeLayout
android:id="@+id/container_hardware_key"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/hardware_key_selection"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_hardware_key_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/hardware_key_checkbox"
android:layout_toRightOf="@+id/hardware_key_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</RelativeLayout>

View File

@@ -30,8 +30,10 @@
<string name="app_name_part2" translatable="false">DX</string>
<string name="app_name_part3" translatable="false">Pro</string>
<string name="app_pro_url" translatable="false">https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro</string>
<string name="contribution_url" translatable="false">https://www.keepassdx.com/contribution</string>
<string name="play_store_url" translatable="false">https://play.google.com/store/apps/details?id=%1$s</string>
<string name="keepro_app_id" translatable="false">com.kunzisoft.keepass.pro</string>
<string name="key_driver_app_id" translatable="false">com.kunzisoft.hardware.key</string>
<string name="contribution_url" translatable="false">https://www.keepassdx.com/#donation</string>
<string name="homepage_url" translatable="false">https://www.keepassdx.com</string>
<string name="external_icon_url" translatable="false">https://www.keepassdx.com/#icons</string>
<string name="issues_url" translatable="false">https://github.com/Kunzisoft/KeePassDX/issues</string>
@@ -91,6 +93,8 @@
<bool name="hide_broken_locations_default" translatable="false">true</bool>
<string name="remember_keyfile_locations_key" translatable="false">remember_keyfile_locations_key</string>
<bool name="remember_keyfile_locations_default" translatable="false">true</bool>
<string name="remember_hardware_key_key" translatable="false">remember_hardware_key_key</string>
<bool name="remember_hardware_key_default" translatable="false">true</bool>
<string name="advanced_unlock_explanation_key" translatable="false">advanced_unlock_explanation_key</string>
<string name="biometric_unlock_enable_key" translatable="false">biometric_unlock_enable_key</string>
<bool name="biometric_unlock_enable_default" translatable="false">false</bool>

View File

@@ -56,6 +56,7 @@
<string name="content_description_otp_information">One-time password info</string>
<string name="content_description_password_checkbox">Password checkbox</string>
<string name="content_description_keyfile_checkbox">Keyfile checkbox</string>
<string name="content_description_hardware_key_checkbox">Hardware key checkbox</string>
<string name="content_description_repeat_toggle_password_visibility">Repeat toggle password visibility</string>
<string name="content_description_entry_icon">Entry icon</string>
<string name="content_description_database_color">Database color</string>
@@ -78,6 +79,8 @@
<string name="content_description_keyboard_close_fields">Close fields</string>
<string name="select_to_copy">Select to copy %1$s to clipboard</string>
<string name="retrieving_db_key">Retrieving database key…</string>
<string name="waiting_challenge_request">Waiting for the challenge request…</string>
<string name="waiting_challenge_response">Waiting for the challenge response…</string>
<string name="database">Database</string>
<string name="template_group_name">Templates</string>
<string name="decrypting_db">Decrypting database content…</string>
@@ -96,6 +99,7 @@
<string name="entry_history">History</string>
<string name="entry_attachments">Attachments</string>
<string name="entry_keyfile">Keyfile</string>
<string name="hardware_key">Hardware key</string>
<string name="entry_modified">Modified</string>
<string name="searchable">Searchable</string>
<string name="inherited">Inherit</string>
@@ -159,8 +163,10 @@
<string name="error_no_name">Enter a name.</string>
<string name="error_word_reserved">This word is reserved and cannot be used.</string>
<string name="error_nokeyfile">Select a keyfile.</string>
<string name="error_no_hardware_key">Select a hardware key.</string>
<string name="error_out_of_memory">No memory to load your entire database.</string>
<string name="error_load_database">Could not load your database.</string>
<string name="error_XML_malformed">XML malformed.</string>
<string name="error_load_database">Could not load the database.</string>
<string name="error_load_database_KDF_memory">Could not load the key. Try to lower the KDF \"Memory Usage\".</string>
<string name="error_pass_gen_type">At least one password generation type must be selected.</string>
<string name="error_disallow_no_credentials">At least one credential must be set.</string>
@@ -176,7 +182,7 @@
<string name="error_copy_group_here">You cannot copy a group here.</string>
<string name="error_create_database">Unable to create database file.</string>
<string name="error_create_database_file">Unable to create database with this password and keyfile.</string>
<string name="error_save_database">Could not save database.</string>
<string name="error_save_database">Could not save the database.</string>
<string name="error_otp_secret_key">Secret key must be in Base32 format.</string>
<string name="error_otp_counter">Counter must be between %1$d and %2$d.</string>
<string name="error_otp_period">Period must be between %1$d and %2$d seconds.</string>
@@ -192,9 +198,19 @@
<string name="error_duplicate_file">The file data already exists.</string>
<string name="error_remove_file">An error occurred while removing the file data.</string>
<string name="error_start_database_action">An error occurred while performing an action on the database.</string>
<string name="error_challenge_already_requested">"Challenge already requested"</string>
<string name="error_response_already_provided">Response already provided.</string>
<string name="error_no_response_from_challenge">Unable to get the response from the challenge.</string>
<string name="error_cancel_by_user">Cancelled by user.</string>
<string name="error_driver_required">Driver for %1$s is required.</string>
<string name="error_unable_merge_database_kdb">Unable to merge from a database V1.</string>
<string name="error_location_unknown">Database location is unknown, database action cannot be performed.</string>
<string name="error_hardware_key_unsupported">Hardware key is not supported.</string>
<string name="error_empty_key">"Key cannot be empty."</string>
<string name="field_name">Field name</string>
<string name="field_value">Field value</string>
<string name="file_not_found_content">Could not find file. Try reopening it from your file browser.</string>
<string name="corrupted_file">Corrupted file.</string>
<string name="file_browser">File manager</string>
<string name="generate_password">Generate password</string>
<string name="hint_conf_pass">Confirm password</string>
@@ -289,6 +305,8 @@
<string name="remember_database_locations_summary">Keeps track of where databases are stored</string>
<string name="remember_keyfile_locations_title">Remember keyfile locations</string>
<string name="remember_keyfile_locations_summary">Keeps track of where keyfiles are stored</string>
<string name="remember_hardware_key_title">Remember hardware keys</string>
<string name="remember_hardware_key_summary">Keeps track of the hardware keys used</string>
<string name="show_recent_files_title">Show recent files</string>
<string name="show_recent_files_summary">Show locations of recent databases</string>
<string name="hide_broken_locations_title">Hide broken database links</string>

View File

@@ -17,8 +17,7 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/general">
@@ -107,6 +106,12 @@
android:summary="@string/remember_keyfile_locations_summary"
android:dependency="@string/remember_database_locations_key"
android:defaultValue="@bool/remember_keyfile_locations_default"/>
<SwitchPreference
android:key="@string/remember_hardware_key_key"
android:title="@string/remember_hardware_key_title"
android:summary="@string/remember_hardware_key_summary"
android:dependency="@string/remember_database_locations_key"
android:defaultValue="@bool/remember_hardware_key_default"/>
<SwitchPreference
android:key="@string/show_recent_files_key"
android:title="@string/show_recent_files_title"

View File

@@ -39,10 +39,11 @@ object HashManager {
return messageDigest
}
fun hashSha256(vararg data: ByteArray): ByteArray {
fun hashSha256(vararg data: ByteArray?): ByteArray {
val hash: MessageDigest = getHash256()
for (byteArray in data) {
hash.update(byteArray)
if (byteArray != null)
hash.update(byteArray)
}
return hash.digest()
}
@@ -57,10 +58,11 @@ object HashManager {
return messageDigest
}
private fun hashSha512(vararg data: ByteArray): ByteArray {
private fun hashSha512(vararg data: ByteArray?): ByteArray {
val hash: MessageDigest = getHash512()
for (byteArray in data) {
hash.update(byteArray)
if (byteArray != null)
hash.update(byteArray)
}
return hash.digest()
}