Merge branch 'develop' into feature/Image_Viewer

This commit is contained in:
J-Jamet
2021-02-07 17:58:48 +01:00
192 changed files with 5479 additions and 2602 deletions

View File

@@ -1,5 +1,61 @@
KeePassDX(2.9.13)
* Fix TOTP plugin settings #878
* Allow Emoji #796
* Scroll and better UI in entry edition screen #876
KeePassDX(2.9.12)
* Fix OTP token type #863
* Fix auto open biometric prompt #862
* Fix back appearance setting #865
* Fix orientation change in settings #872
* Change memory unit to MiB #851
* Small changes #642
KeePassDX(2.9.11)
* Add Keyfile XML version 2 (fix hex) #844
* Fix hex Keyfile #861
KeePassDX(2.9.10)
* Try to fix autofill #852
* Fix database change dialog displayed too often #853
KeePassDX(2.9.9)
* Detect file changes and reload database #794
* Inline suggestions autofill with compatible keyboard (Android R) #827
* Add Keyfile XML version 2 #844
* Fix binaries of 64 bytes #835
* Special search in title fields #830
* Priority to OTP button in notifications #845
* Fix OTP generation for long secret key #848
* Fix small bugs #849
KeePassDX(2.9.8)
* Fix specific attachments with kdbx3.1 databases #828
* Fix small bugs
KeePassDX(2.9.7)
* Remove write permission since Android 10 #823
* Fix small bugs
KeePassDX(2.9.6)
* Fix KeyFile bug #820
KeePassDX(2.9.5)
* Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
* Prevent auto switch back to previous keyboard if otp field exists #814
* Fix timeout reset #817
KeePassDX(2.9.4)
* Fix small bugs #812
* Argon2ID implementation #791
KeePassDX(2.9.3)
* Unlock database by device credentials (PIN/Password/Pattern) #779 #102
* Advanced unlock with timeout #102 #437 #566
* Remove default database parameter when the file is no longer accessible #803
* Move OTP button to the first view level in Magikeyboard #587
* Tooltips for Magikeyboard #586
* Fix small bugs #805
KeePassDX(2.9.2)
* Managing OTP links from QR applications #556

View File

@@ -5,22 +5,22 @@ apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 30
buildToolsVersion '30.0.2'
buildToolsVersion '30.0.3'
ndkVersion '21.3.6528147'
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 30
versionCode = 47
versionName = "2.9.3"
versionCode = 57
versionName = "2.9.13"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
kapt {
arguments {
@@ -92,7 +92,7 @@ android {
}
}
def room_version = "2.2.5"
def room_version = "2.2.6"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -110,6 +110,8 @@ dependencies {
// Database
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// Autofill
implementation "androidx.autofill:autofill:1.1.0-rc01"
// Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
// Time
@@ -121,7 +123,7 @@ dependencies {
// Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.2'
// Apache Commons Codec
implementation 'commons-codec:commons-codec:1.14'
implementation 'commons-codec:commons-codec:1.15'
// Icon pack
implementation project(path: ':icon-pack-classic')
implementation project(path: ':icon-pack-material')

View File

@@ -14,10 +14,15 @@
android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.VIBRATE"/>
<!-- Write permission until Android 10 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- Open apps from links -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"/>
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:label="@string/app_name"
@@ -129,7 +134,7 @@
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
android:windowSoftInputMode="adjustResize" />
<!-- About and Settings -->
<activity
android:name="com.kunzisoft.keepass.activities.AboutActivity"
@@ -173,15 +178,19 @@
</activity>
<service
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name=".notifications.AttachmentFileNotificationService"
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Autofill -->
@@ -207,7 +216,7 @@
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true"
android:exported="false" />

View File

@@ -26,6 +26,7 @@ import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
@@ -33,6 +34,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
@@ -40,7 +42,6 @@ import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.UriUtil
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : AppCompatActivity() {
@@ -84,9 +85,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
private fun launchSelection(searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (assistStructure == null) {
if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED)
finish()
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
@@ -105,21 +106,21 @@ class AutofillLauncherActivity : AppCompatActivity() {
searchInfo,
{ items ->
// Items found
AutofillHelper.buildResponse(this, items)
AutofillHelper.buildResponseAndSetResult(this, items)
finish()
},
{
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
readOnly,
assistStructure,
autofillComponent,
searchInfo,
false)
},
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
assistStructure,
autofillComponent,
searchInfo)
}
)
@@ -196,7 +197,8 @@ class AutofillLauncherActivity : AppCompatActivity() {
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getAuthIntentSenderForSelection(context: Context,
searchInfo: SearchInfo? = null): IntentSender {
searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply {
@@ -205,6 +207,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
},
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
}

View File

@@ -39,7 +39,9 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
@@ -49,20 +51,19 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.createDocument
import com.kunzisoft.keepass.utils.onCreateDocumentResult
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.EntryContentsView
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import java.util.*
import kotlin.collections.HashMap
@@ -133,7 +134,7 @@ class EntryActivity : LockingActivity() {
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Init the clipboard helper
clipboardHelper = ClipboardHelper(this)
@@ -150,8 +151,13 @@ class EntryActivity : LockingActivity() {
if (result.isSuccess)
finish()
}
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
this.showActionErrorIfNeeded(result)
finish()
}
}
coordinatorLayout?.showActionError(result)
coordinatorLayout?.showActionErrorIfNeeded(result)
}
}
@@ -198,8 +204,7 @@ class EntryActivity : LockingActivity() {
// Refresh Menu
invalidateOptionsMenu()
val entryInfo = entry.getEntryInfo(Database.getInstance())
val entryInfo = entry.getEntryInfo(mDatabase)
// Manage entry copy to start notification if allowed
if (mFirstLaunchOfActivity) {
// Manage entry to launch copying notification if allowed
@@ -231,23 +236,21 @@ class EntryActivity : LockingActivity() {
private fun fillEntryDataInContentsView(entry: Entry) {
val database = Database.getInstance()
database.startManageEntry(entry)
val entryInfo = entry.getEntryInfo(mDatabase)
// Assign title icon
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor)
// Assign title text
val entryTitle = entry.title
val entryTitle = entryInfo.title
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entry.username) {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.username,
entryContentsView?.assignUserName(entryInfo.username) {
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
database.stopManageEntry(entry)
}
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
@@ -277,11 +280,9 @@ class EntryActivity : LockingActivity() {
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.password,
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
getString(R.string.copy_field,
getString(R.string.entry_password)))
database.stopManageEntry(entry)
}
} else {
// If dialog not already shown
@@ -291,44 +292,46 @@ class EntryActivity : LockingActivity() {
null
}
}
entryContentsView?.assignPassword(entry.password,
entryContentsView?.assignPassword(entryInfo.password,
allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
View.OnClickListener {
entry.getOtpElement()?.let { otpElement ->
clipboardHelper?.timeoutCopyToClipboard(
otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
)
}
})
entry.getOtpElement()?.let { otpElement ->
entryContentsView?.assignOtp(otpElement, entryProgress) {
clipboardHelper?.timeoutCopyToClipboard(
otpElement.token,
getString(R.string.copy_field, getString(R.string.entry_otp))
)
}
}
entryContentsView?.assignURL(entry.url)
entryContentsView?.assignNotes(entry.notes)
entryContentsView?.assignURL(entryInfo.url)
entryContentsView?.assignNotes(entryInfo.notes)
// Assign custom fields
if (mDatabase?.allowEntryCustomFields() == true) {
entryContentsView?.clearExtraFields()
entry.getExtraFields().forEach { field ->
entryInfo.customFields.forEach { field ->
val label = field.name
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
// OTP field is already managed in dedicated view
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
val value = field.protectedValue
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
}
} else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
} else {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
}
}
}
}
@@ -336,24 +339,16 @@ class EntryActivity : LockingActivity() {
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
mDatabase?.binaryPool?.let { binaryPool ->
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
// Assign dates
entryContentsView?.assignCreationDate(entry.creationTime)
entryContentsView?.assignModificationDate(entry.lastModificationTime)
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
entryContentsView?.setExpires(entry.isCurrentlyExpires)
if (entry.expires) {
entryContentsView?.assignExpiresDate(entry.expiryTime)
} else {
entryContentsView?.assignExpiresDate(getString(R.string.never))
}
entryContentsView?.assignCreationDate(entryInfo.creationTime)
entryContentsView?.assignModificationDate(entryInfo.modificationTime)
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
// Manage history
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
@@ -368,8 +363,6 @@ class EntryActivity : LockingActivity() {
// Assign special data
entryContentsView?.assignUUID(entry.nodeId.id)
database.stopManageEntry(entry)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -407,6 +400,9 @@ class EntryActivity : LockingActivity() {
menu.findItem(R.id.menu_save_database)?.isVisible = false
menu.findItem(R.id.menu_edit)?.isVisible = false
}
if (mSpecialMode != SpecialMode.DEFAULT) {
menu.findItem(R.id.menu_reload_database)?.isVisible = false
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
@@ -500,6 +496,9 @@ class EntryActivity : LockingActivity() {
R.id.menu_save_database -> {
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
}
R.id.menu_reload_database -> {
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
}
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
}
return super.onOptionsItemSelected(item)

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -37,7 +36,6 @@ import android.widget.DatePicker
import android.widget.TimePicker
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
@@ -48,6 +46,8 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
@@ -56,21 +56,22 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.ToolbarAction
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import org.joda.time.DateTime
import java.util.*
@@ -97,7 +98,7 @@ class EntryEditActivity : LockingActivity(),
private var coordinatorLayout: CoordinatorLayout? = null
private var scrollView: NestedScrollView? = null
private var entryEditFragment: EntryEditFragment? = null
private var entryEditAddToolBar: Toolbar? = null
private var entryEditAddToolBar: ToolbarAction? = null
private var validateButton: View? = null
private var lockView: View? = null
@@ -117,11 +118,12 @@ class EntryEditActivity : LockingActivity(),
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_entry_edit)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
setSupportActionBar(entryEditAddToolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
@@ -134,7 +136,7 @@ class EntryEditActivity : LockingActivity(),
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
@@ -234,51 +236,6 @@ class EntryEditActivity : LockingActivity(),
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
}
// Assign title
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
// Bottom Bar
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu.findItem(R.id.menu_add_attachment).isVisible = false
}
menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_add_field -> {
addNewCustomField()
true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> {
setupOTP()
true
}
else -> true
}
}
}
// To retrieve attachment
mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -334,8 +291,13 @@ class EntryEditActivity : LockingActivity(),
Log.e(TAG, "Unable to retrieve entry after database action", e)
}
}
ACTION_DATABASE_RELOAD_TASK -> {
// Close the current activity
this.showActionErrorIfNeeded(result)
finish()
}
}
coordinatorLayout?.showActionError(result)
coordinatorLayout?.showActionErrorIfNeeded(result)
}
}
@@ -360,7 +322,7 @@ class EntryEditActivity : LockingActivity(),
// Build Autofill response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDatabase?.let { database ->
AutofillHelper.buildResponse(this@EntryEditActivity,
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
entry.getEntryInfo(database))
}
}
@@ -478,8 +440,12 @@ class EntryEditActivity : LockingActivity(),
}
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
verifyNameField(newField) {
if (oldField.name.equals(newField.name, true)) {
entryEditFragment?.replaceExtraField(oldField, newField)
} else {
verifyNameField(newField) {
entryEditFragment?.replaceExtraField(oldField, newField)
}
}
}
@@ -609,18 +575,30 @@ class EntryEditActivity : LockingActivity(),
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
inflater.inflate(R.menu.database, menu)
// Save database not needed here
menu.findItem(R.id.menu_save_database)?.isVisible = false
MenuUtil.contributionMenuInflater(inflater, menu)
menuInflater.inflate(R.menu.entry_edit, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_add_field)?.apply {
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
isEnabled = allowCustomField
isVisible = allowCustomField
}
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu?.findItem(R.id.menu_add_attachment)?.isVisible = false
}
menu?.findItem(R.id.menu_add_otp)?.apply {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
entryEditActivityEducation?.let {
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
}
@@ -672,11 +650,16 @@ class EntryEditActivity : LockingActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save_database -> {
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
R.id.menu_add_field -> {
addNewCustomField()
return true
}
R.id.menu_contribute -> {
MenuUtil.onContributionItemSelected(this)
R.id.menu_add_attachment -> {
addNewAttachment(item)
return true
}
R.id.menu_add_otp -> {
setupOTP()
return true
}
android.R.id.home -> {
@@ -908,7 +891,7 @@ class EntryEditActivity : LockingActivity(),
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
autofillComponent: AutofillComponent,
group: Group,
searchInfo: SearchInfo? = null) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
@@ -916,7 +899,7 @@ class EntryEditActivity : LockingActivity(),
intent.putExtra(KEY_PARENT, group.nodeId)
AutofillHelper.startActivityForAutofillResult(activity,
intent,
assistStructure,
autofillComponent,
searchInfo)
}
}

View File

@@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
@@ -148,6 +149,8 @@ class EntryEditFragment: StylishFragment() {
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
taIconColor?.recycle()
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
// Retrieve the new entry after an orientation change
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -36,7 +35,6 @@ import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -49,16 +47,18 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError
@@ -162,7 +162,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
// Observe list of databases
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
when (databaseFiles.databaseFileAction) {
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
@@ -186,13 +186,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
}
databaseFilesViewModel.consumeAction()
})
}
// Observe default database
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
databaseFilesViewModel.defaultDatabase.observe(this) {
// Retrieve settings for default database
mAdapterDatabaseHistory?.setDefaultDatabase(it)
})
}
// Attach the dialog thread to this activity
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
@@ -200,8 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (actionTask) {
ACTION_DATABASE_CREATE_TASK -> {
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
}
}
ACTION_DATABASE_LOAD_TASK -> {
@@ -237,10 +237,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private fun fileNoFoundAction(e: FileNotFoundException) {
val error = getString(R.string.file_not_found_content)
Log.e(TAG, error, e)
coordinatorLayout?.let {
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
}
Log.e(TAG, error, e)
}
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
@@ -331,9 +331,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
}
override fun onAssignKeyDialogPositiveClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
try {
mDatabaseFileUri?.let { databaseUri ->
@@ -341,10 +339,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Create the new database
mProgressDatabaseTaskProvider?.startDatabaseCreate(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
mainCredential
)
}
} catch (e: Exception) {
@@ -354,11 +349,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
}
override fun onAssignKeyDialogNegativeClick(
masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?) {
}
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@@ -435,8 +426,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
return super.onOptionsItemSelected(item)
}
companion object {
@@ -502,11 +493,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
assistStructure: AssistStructure,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
assistStructure,
autofillComponent,
searchInfo)
}

View File

@@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.SearchManager
import android.app.assist.AssistStructure
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -50,7 +49,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
@@ -65,15 +66,16 @@ import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
@@ -153,7 +155,7 @@ class GroupActivity : LockingActivity(),
taTextColor.recycle()
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView)
rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this)
// Retrieve elements after an orientation change
if (savedInstanceState != null) {
@@ -227,10 +229,10 @@ class GroupActivity : LockingActivity(),
currentGroup, searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo, assistStructure ->
{ searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EntryEditActivity.launchForAutofillResult(this@GroupActivity,
assistStructure,
autofillComponent,
currentGroup, searchInfo)
onLaunchActivitySpecialMode()
} else {
@@ -316,7 +318,12 @@ class GroupActivity : LockingActivity(),
if (result.isSuccess) {
// Rebuild all the list to avoid bug when delete node from sort
mListNodesFragment?.rebuildList()
try {
mListNodesFragment?.rebuildList()
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list after deletion")
e.printStackTrace()
}
// Add trash in views list if it doesn't exists
if (database.isRecycleBinEnabled) {
@@ -336,9 +343,20 @@ class GroupActivity : LockingActivity(),
}
}
}
ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
if (result.isSuccess) {
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
} else {
this.showActionErrorIfNeeded(result)
finish()
}
}
}
coordinatorLayout?.showActionError(result)
coordinatorLayout?.showActionErrorIfNeeded(result)
finishNodeAction()
@@ -659,7 +677,7 @@ class GroupActivity : LockingActivity(),
// Build response with the entry selected
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
mDatabase?.let { database ->
AutofillHelper.buildResponse(this,
AutofillHelper.buildResponseAndSetResult(this,
entry.getEntryInfo(database))
}
}
@@ -872,6 +890,8 @@ class GroupActivity : LockingActivity(),
}
if (mSpecialMode == SpecialMode.DEFAULT) {
MenuUtil.defaultMenuInflater(inflater, menu)
} else {
menu.findItem(R.id.menu_reload_database)?.isVisible = false
}
// Menu for recycle bin
@@ -997,6 +1017,10 @@ class GroupActivity : LockingActivity(),
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
return true
}
R.id.menu_reload_database -> {
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
return true
}
R.id.menu_empty_recycle_bin -> {
mCurrentGroup?.getChildren()?.let { listChildren ->
// Automatically delete all elements
@@ -1124,7 +1148,16 @@ class GroupActivity : LockingActivity(),
private fun rebuildListNodes() {
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment?
// to refresh fragment
mListNodesFragment?.rebuildList()
try {
mListNodesFragment?.rebuildList()
} catch (e: Exception) {
e.printStackTrace()
coordinatorLayout?.let { coordinatorLayout ->
Snackbar.make(coordinatorLayout,
R.string.error_rebuild_list,
Snackbar.LENGTH_LONG).asError().show()
}
}
mCurrentGroup = mListNodesFragment?.mainGroup
// Remove search in intent
deletePreviousSearchGroup()
@@ -1295,14 +1328,14 @@ class GroupActivity : LockingActivity(),
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
readOnly: Boolean,
assistStructure: AssistStructure,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) {
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
AutofillHelper.startActivityForAutofillResult(activity,
intent,
assistStructure,
autofillComponent,
searchInfo)
}
}
@@ -1419,21 +1452,21 @@ class GroupActivity : LockingActivity(),
}
)
},
{ searchInfo, assistStructure ->
{ searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SearchHelper.checkAutoSearchInfo(activity,
Database.getInstance(),
searchInfo,
{ items ->
// Response is build
AutofillHelper.buildResponse(activity, items)
AutofillHelper.buildResponseAndSetResult(activity, items)
onValidateSpecialMode()
},
{
// Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(activity,
readOnly,
assistStructure,
autofillComponent,
searchInfo,
false)
onLaunchActivitySpecialMode()

View File

@@ -22,30 +22,24 @@ package com.kunzisoft.keepass.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.*
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.NodeAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.util.*
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
@@ -197,7 +191,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
// Refresh data
rebuildList()
try {
rebuildList()
} catch (e: Exception) {
Log.e(TAG, "Unable to rebuild the list during resume")
e.printStackTrace()
}
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
// To show the " no search entry found "
@@ -209,10 +208,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
}
@Throws(IllegalArgumentException::class)
fun rebuildList() {
// Add elements to the list
mainGroup?.let { mainGroup ->
mAdapter?.apply {
// Thrown an exception when sort cannot be performed
rebuildList(mainGroup)
// To visually change the elements
if (PreferencesUtil.APPEARANCE_CHANGED) {
@@ -231,8 +232,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
}
// Tell the adapter to refresh it's list
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
try {
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
rebuildList()
} catch (e:Exception) {
Log.e(TAG, "Unable to rebuild the list with the sort")
e.printStackTrace()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.app.assist.AssistStructure
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -37,9 +36,8 @@ import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricManager
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Observer
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
@@ -50,33 +48,33 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import kotlinx.android.synthetic.main.activity_password.*
import java.io.FileNotFoundException
open class PasswordActivity : SpecialModeActivity() {
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views
private var toolbar: Toolbar? = null
@@ -86,12 +84,12 @@ open class PasswordActivity : SpecialModeActivity() {
private var confirmButtonView: Button? = null
private var checkboxPasswordView: CompoundButton? = null
private var checkboxKeyFileView: CompoundButton? = null
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private var infoContainerView: ViewGroup? = null
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null
private var mDatabaseKeyFileUri: Uri? = null
@@ -113,7 +111,6 @@ open class PasswordActivity : SpecialModeActivity() {
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
private var mAllowAutoOpenBiometricPrompt: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) {
@@ -133,7 +130,6 @@ open class PasswordActivity : SpecialModeActivity() {
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
advancedUnlockInfoView = findViewById(R.id.biometric_info)
infoContainerView = findViewById(R.id.activity_password_info_container)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
@@ -160,10 +156,6 @@ open class PasswordActivity : SpecialModeActivity() {
}
})
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
enableOrNotTheConfirmationButton()
}
// If is a view intent
getUriFromIntent(intent)
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
@@ -173,8 +165,31 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Init Biometric elements
advancedUnlockFragment = supportFragmentManager
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
if (advancedUnlockFragment == null) {
advancedUnlockFragment = AdvancedUnlockFragment()
supportFragmentManager.commit {
replace(R.id.fragment_advanced_unlock_container_view,
advancedUnlockFragment!!,
UNLOCK_FRAGMENT_TAG)
}
}
// Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
advancedUnlockFragment?.checkUnlockAvailability()
enableOrNotTheConfirmationButton()
}
// Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase
}
// Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists
mForceReadOnly = databaseFile?.let {
!it.databaseFileExists
@@ -194,19 +209,14 @@ open class PasswordActivity : SpecialModeActivity() {
filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
})
}
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
onActionFinish = { actionTask, result ->
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isAdvancedUnlockEnable(this@PasswordActivity)) {
// Stay with the same mode and init it
advancedUnlockedManager?.initAdvancedUnlockMode()
}
}
advancedUnlockFragment?.initAdvancedUnlockMode()
if (result.isSuccess) {
mDatabaseKeyFileUri = null
@@ -220,32 +230,37 @@ open class PasswordActivity : SpecialModeActivity() {
if (resultException != null) {
resultError = resultException.getLocalizedMessage(resources)
// Relaunch loading if we need to fix UUID
if (resultException is DuplicateUuidDatabaseException) {
showLoadDatabaseDuplicateUuidMessage {
when (resultException) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var databaseUri: Uri? = null
var masterPassword: String? = null
var keyFileUri: Uri? = null
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
var databaseUri: Uri? = null
var mainCredential: MainCredential = MainCredential()
var readOnly = true
var cipherEntity: CipherDatabaseEntity? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
mainCredential,
readOnly,
cipherEntity,
true)
}
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
masterPassword,
keyFileUri,
readOnly,
cipherEntity,
true)
}
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase()
}
}
}
@@ -277,6 +292,9 @@ open class PasswordActivity : SpecialModeActivity() {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
mDatabaseFileUri?.let {
databaseFileViewModel.checkIfIsDefaultDatabase(it)
}
}
override fun onNewIntent(intent: Intent?) {
@@ -303,6 +321,33 @@ open class PasswordActivity : SpecialModeActivity() {
finish()
}
override fun retrieveCredentialForEncryption(): String {
return passwordView?.text?.toString() ?: ""
}
override fun conditionToStoreCredential(): Boolean {
return checkboxPasswordView?.isChecked == true
}
override fun onCredentialEncrypted(databaseUri: Uri,
encryptedCredential: String,
ivSpec: String) {
// Load the database if password is registered with biometric
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseUri.toString(),
encryptedCredential,
ivSpec)
)
}
override fun onCredentialDecrypted(databaseUri: Uri,
decryptedCredential: String) {
// Load the database if password is retrieve from biometric
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
}
private val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == IME_ACTION_DONE) {
@@ -369,48 +414,9 @@ open class PasswordActivity : SpecialModeActivity() {
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else {
// Init Biometric elements
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
if (advancedUnlockedManager == null
&& databaseFileUri != null) {
advancedUnlockedManager = AdvancedUnlockedManager(this,
databaseFileUri,
advancedUnlockInfoView,
checkboxPasswordView,
enableButtonOnCheckedChangeListener,
passwordView,
{ passwordEncrypted, ivSpec ->
// Load the database if password is registered with biometric
if (passwordEncrypted != null && ivSpec != null) {
verifyCheckboxesAndLoadDatabase(
CipherDatabaseEntity(
databaseFileUri.toString(),
passwordEncrypted,
ivSpec)
)
}
},
{ passwordDecrypted ->
// Load the database if password is retrieve from biometric
passwordDecrypted?.let {
// Retrieve from biometric
verifyKeyFileCheckboxAndLoadDatabase(it)
}
})
}
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
advancedUnlockedManager?.checkBiometricAvailability()
} else {
advancedUnlockInfoView?.visibility = View.GONE
advancedUnlockedManager?.destroy()
advancedUnlockedManager = null
}
}
if (advancedUnlockedManager == null) {
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
}
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
advancedUnlockFragment?.loadDatabase(databaseFileUri,
mAllowAutoOpenBiometricPrompt
&& mProgressDatabaseTaskProvider?.isBinded() != true)
}
enableOrNotTheConfirmationButton()
@@ -462,11 +468,6 @@ open class PasswordActivity : SpecialModeActivity() {
override fun onPause() {
mProgressDatabaseTaskProvider?.unregisterProgressTask()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.destroy()
advancedUnlockedManager = null
}
// Reinit locking activity UI variable
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
@@ -530,8 +531,7 @@ open class PasswordActivity : SpecialModeActivity() {
// Show the progress dialog and load the database
showProgressDialogAndLoadDatabase(
databaseUri,
password,
keyFileUri,
MainCredential(password, keyFileUri),
readOnly,
cipherDatabaseEntity,
false)
@@ -540,15 +540,13 @@ open class PasswordActivity : SpecialModeActivity() {
}
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
password: String?,
keyFile: Uri?,
mainCredential: MainCredential,
readOnly: Boolean,
cipherDatabaseEntity: CipherDatabaseEntity?,
fixDuplicateUUID: Boolean) {
mProgressDatabaseTaskProvider?.startDatabaseLoad(
databaseUri,
password,
keyFile,
mainCredential,
readOnly,
cipherDatabaseEntity,
fixDuplicateUUID
@@ -575,11 +573,6 @@ open class PasswordActivity : SpecialModeActivity() {
MenuUtil.defaultMenuInflater(inflater, menu)
}
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
}
super.onCreateOptionsMenu(menu)
launchEducation(menu)
@@ -589,13 +582,13 @@ open class PasswordActivity : SpecialModeActivity() {
// Check permission
private fun checkPermission() {
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (Build.VERSION.SDK_INT >= 23
if (Build.VERSION.SDK_INT in 23..28
&& !readOnly
&& !mPermissionAsked) {
mPermissionAsked = true
// Check self permission to show or not the dialog
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
val permissions = arrayOf(writePermission)
if (toolbar != null
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
@@ -655,21 +648,14 @@ open class PasswordActivity : SpecialModeActivity() {
performedNextEducation(passwordActivityEducation, menu)
})
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
PreferencesUtil.isAdvancedUnlockEnable(applicationContext)
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE
&& advancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
})
}
advancedUnlockFragment?.performEducation(passwordActivityEducation,
readOnlyEducationPerformed,
{
performedNextEducation(passwordActivityEducation, menu)
},
{
performedNextEducation(passwordActivityEducation, menu)
})
}
}
@@ -691,10 +677,7 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly = !readOnly
changeOpenFileReadIcon(item)
}
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
advancedUnlockedManager?.deleteEntryKey()
}
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
}
return super.onOptionsItemSelected(item)
@@ -708,6 +691,9 @@ open class PasswordActivity : SpecialModeActivity() {
mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
@@ -728,7 +714,7 @@ open class PasswordActivity : SpecialModeActivity() {
when (resultCode) {
LockingActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
@@ -741,6 +727,8 @@ open class PasswordActivity : SpecialModeActivity() {
private val TAG = PasswordActivity::class.java.name
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile"
private const val VIEW_INTENT = "android.intent.action.VIEW"
@@ -844,13 +832,13 @@ open class PasswordActivity : SpecialModeActivity() {
fun launchForAutofillResult(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
assistStructure: AssistStructure,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
assistStructure,
autofillComponent,
searchInfo)
}
}
@@ -908,11 +896,11 @@ open class PasswordActivity : SpecialModeActivity() {
searchInfo)
onLaunchActivitySpecialMode()
},
{ searchInfo, assistStructure -> // Autofill Selection Action
{ searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile,
assistStructure,
autofillComponent,
searchInfo)
onLaunchActivitySpecialMode()
} else {

View File

@@ -37,6 +37,7 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
@@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
interface AssignPasswordDialogListener {
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
keyFileChecked: Boolean, keyFile: Uri?)
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
}
override fun onAttach(activity: Context) {
@@ -161,17 +160,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
}
@@ -183,6 +178,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
return super.onCreateDialog(savedInstanceState)
}
private inline fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
return MainCredential(masterPassword, keyFile)
}
override fun onResume() {
super.onResume()
@@ -242,9 +243,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) {
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss()
}
}
@@ -259,9 +258,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_no_encryption_key)
.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAssignKeyDialogPositiveClick(
passwordCheckBox!!.isChecked, mMasterPassword,
keyFileCheckBox!!.isChecked, mKeyFile)
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
this@AssignMasterKeyDialogFragment.dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2020 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.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
class DatabaseChangedDialogFragment : DialogFragment() {
var actionDatabaseListener: ActionDatabaseChangedListener? = null
override fun onPause() {
super.onPause()
actionDatabaseListener = null
this.dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder()
if (newSnapFileDatabaseInfo.exists) {
stringBuilder.append(getString(R.string.warning_database_info_changed))
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
+ "\n\n" +
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
} else {
stringBuilder.append(getString(R.string.warning_database_revoked))
}
builder.setMessage(stringBuilder)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
actionDatabaseListener?.validateDatabaseChanged()
}
return builder.create()
}
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionDatabaseChangedListener {
fun validateDatabaseChanged()
}
companion object {
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
: DatabaseChangedDialogFragment {
val fragment = DatabaseChangedDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
}
return fragment
}
}
}

View File

@@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
open class DeleteNodesDialogFragment : DialogFragment() {

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {

View File

@@ -26,6 +26,7 @@ import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() {
@@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
@@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
builder.setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onPasswordEncodingValidateListener(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
mainCredential
)
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
@@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() {
interface Listener {
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?)
mainCredential: MainCredential)
}
companion object {
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
fun getInstance(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?): SortDialogFragment {
mainCredential: MainCredential): SortDialogFragment {
val fragment = SortDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(MASTER_PASSWORD_KEY, masterPassword)
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(KEY_FILE_URI_KEY, keyFile)
putParcelable(MAIN_CREDENTIAL, mainCredential)
}
return fragment
}

View File

@@ -29,10 +29,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
@@ -57,6 +54,7 @@ class SetOTPDialogFragment : DialogFragment() {
private var mOtpElement: OtpElement = OtpElement()
private var otpTypeMessage: TextView? = null
private var otpTypeSpinner: Spinner? = null
private var otpTokenTypeSpinner: Spinner? = null
private var otpSecretContainer: TextInputLayout? = null
@@ -74,6 +72,8 @@ class SetOTPDialogFragment : DialogFragment() {
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
private var mManualEvent = false
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
@@ -134,6 +134,7 @@ class SetOTPDialogFragment : DialogFragment() {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
@@ -183,23 +184,23 @@ class SetOTPDialogFragment : DialogFragment() {
// HOTP / TOTP Type selection
val otpTypeArray = OtpType.values()
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
otpTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, otpTypeArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
otpTypeSpinner?.adapter = otpTypeAdapter
// Otp Token type selection
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
hotpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
// Proprietary only on closed and full version
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
totpTokenTypeAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
otpTokenTypeAdapter = hotpTokenTypeAdapter
@@ -207,7 +208,7 @@ class SetOTPDialogFragment : DialogFragment() {
// OTP Algorithm
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
otpAlgorithmAdapter = ArrayAdapter(activity,
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
@@ -372,24 +373,40 @@ class SetOTPDialogFragment : DialogFragment() {
}
private fun upgradeTokenType() {
val tokenType = mOtpElement.tokenType
when (mOtpElement.type) {
OtpType.HOTP -> {
otpPeriodContainer?.visibility = View.GONE
otpCounterContainer?.visibility = View.VISIBLE
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
mHotpTokenTypeArray?.let { otpTokenTypeArray ->
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
}
}
OtpType.TOTP -> {
otpPeriodContainer?.visibility = View.VISIBLE
otpCounterContainer?.visibility = View.GONE
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
otpTokenTypeSpinner?.setSelection(OtpTokenType
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
mTotpTokenTypeArray?.let { otpTokenTypeArray ->
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
}
}
}
}
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
tokenType: OtpTokenType,
defaultTokenType: OtpTokenType) {
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
otpTypeMessage?.visibility = View.GONE
tokenType
} else {
otpTypeMessage?.visibility = View.VISIBLE
defaultTokenType
}
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
}
private fun upgradeParameters() {
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
.indexOf(mOtpElement.algorithm))

View File

@@ -19,10 +19,10 @@
*/
package com.kunzisoft.keepass.activities.helpers
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.os.Build
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
@@ -106,7 +106,7 @@ object EntrySelectionHelper {
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAssistStructure(intent) != null)
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return SpecialMode.SELECTION
}
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
@@ -119,7 +119,7 @@ object EntrySelectionHelper {
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (AutofillHelper.retrieveAssistStructure(intent) != null)
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
return TypeMode.AUTOFILL
}
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
@@ -136,7 +136,7 @@ object EntrySelectionHelper {
saveAction: (searchInfo: SearchInfo) -> Unit,
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?,
assistStructure: AssistStructure) -> Unit,
autofillComponent: AutofillComponent) -> Unit,
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) {
@@ -167,14 +167,14 @@ object EntrySelectionHelper {
}
SpecialMode.SELECTION -> {
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
var assistStructureInit = false
var autofillComponentInit = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
autofillSelectionAction.invoke(searchInfo, assistStructure)
assistStructureInit = true
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
autofillSelectionAction.invoke(searchInfo, autofillComponent)
autofillComponentInit = true
}
}
if (!assistStructureInit) {
if (!autofillComponentInit) {
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
when (retrieveTypeModeFromIntent(intent)) {
TypeMode.DEFAULT -> {

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.activities.lock
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
@@ -59,6 +60,9 @@ abstract class LockingActivity : SpecialModeActivity() {
private set
override fun onCreate(savedInstanceState: Bundle?) {
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
super.onCreate(savedInstanceState)
if (savedInstanceState != null
@@ -83,8 +87,6 @@ abstract class LockingActivity : SpecialModeActivity() {
}
mExitLock = false
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -163,35 +165,6 @@ abstract class LockingActivity : SpecialModeActivity() {
sendBroadcast(Intent(LOCK_ACTION))
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
views.forEach {
it?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
false
}
it?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
// Log.d(TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
}
}
if (it is ViewGroup) {
for (i in 0..it.childCount) {
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
}
}
}
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
@@ -204,7 +177,7 @@ abstract class LockingActivity : SpecialModeActivity() {
companion object {
private const val TAG = "LockingActivity"
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
@@ -215,3 +188,28 @@ abstract class LockingActivity : SpecialModeActivity() {
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
}
}
/**
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
}
false
}
setOnFocusChangeListener { _, _ ->
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
}
}
}

View File

@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.view.SpecialModeView
abstract class SpecialModeActivity : StylishActivity() {
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mTypeMode: TypeMode = TypeMode.DEFAULT
private var mSpecialModeView: SpecialModeView? = null

View File

@@ -34,7 +34,7 @@ class App : MultiDexApplication() {
}
override fun onTerminate() {
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
super.onTerminate()
}
}

View File

@@ -19,27 +19,96 @@
*/
package com.kunzisoft.keepass.app.database
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter
import java.util.*
class CipherDatabaseAction(applicationContext: Context) {
class CipherDatabaseAction(context: Context) {
private val applicationContext = context.applicationContext
private val cipherDatabaseDao =
AppDatabase
.getDatabase(applicationContext)
.cipherDatabaseDao()
// Temp DAO to easily remove content if object no longer in memory
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
private val mIntentAdvancedUnlockService = Intent(applicationContext,
AdvancedUnlockNotificationService::class.java)
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
private var mServiceConnection: ServiceConnection? = null
private var mDatabaseListeners = LinkedList<DatabaseListener>()
fun reloadPreferences() {
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
}
@Synchronized
private fun attachService(performedAction: () -> Unit) {
// Check if a service is currently running else do nothing
if (mBinder != null) {
performedAction.invoke()
} else if (mServiceConnection == null) {
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
performedAction.invoke()
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder = null
mServiceConnection = null
mDatabaseListeners.forEach {
it.onDatabaseCleared()
}
}
}
applicationContext.bindService(mIntentAdvancedUnlockService,
mServiceConnection!!,
Context.BIND_ABOVE_CLIENT)
if (mBinder == null) {
applicationContext.startService(mIntentAdvancedUnlockService)
}
}
}
fun registerDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.add(listener)
}
fun unregisterDatabaseListener(listener: DatabaseListener) {
mDatabaseListeners.remove(listener)
}
interface DatabaseListener {
fun onDatabaseCleared()
}
fun getCipherDatabase(databaseUri: Uri,
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener.invoke(it)
}
).execute()
if (useTempDao) {
attachService {
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
}
} else {
IOActionTask(
{
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener.invoke(it)
}
).execute()
}
}
fun containsCipherDatabase(databaseUri: Uri,
@@ -51,36 +120,52 @@ class CipherDatabaseAction(applicationContext: Context) {
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
cipherDatabaseResultListener: (() -> Unit)? = null) {
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity)
} else {
cipherDatabaseDao.update(cipherDatabaseEntity)
if (useTempDao) {
attachService {
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
// Update values if element not yet in the database
if (cipherDatabaseRetrieve == null) {
cipherDatabaseDao.add(cipherDatabaseEntity)
} else {
cipherDatabaseDao.update(cipherDatabaseEntity)
}
},
{
cipherDatabaseResultListener?.invoke()
}
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
).execute()
}
}
fun deleteByDatabaseUri(databaseUri: Uri,
cipherDatabaseResultListener: (() -> Unit)? = null) {
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
if (useTempDao) {
attachService {
mBinder?.deleteByDatabaseUri(databaseUri)
cipherDatabaseResultListener?.invoke()
}
} else {
IOActionTask(
{
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
},
{
cipherDatabaseResultListener?.invoke()
}
).execute()
}
}
fun deleteAll() {
attachService {
mBinder?.deleteAll()
}
IOActionTask(
{
cipherDatabaseDao.deleteAll()

View File

@@ -43,6 +43,11 @@ data class CipherDatabaseEntity(
parcel.readString()!!,
parcel.readString()!!)
fun replaceContent(copy: CipherDatabaseEntity) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(databaseUri)
parcel.writeString(encryptedValue)

View File

@@ -47,7 +47,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(),
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
},
@@ -90,7 +90,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(),
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
)
@@ -152,7 +152,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.exists,
fileDatabaseInfo.getModificationString(),
fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
)
}

View File

@@ -34,7 +34,12 @@ class IOActionTask<T>(
mainScope.launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<T?> = async {
action.invoke()
try {
action.invoke()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await())

View File

@@ -0,0 +1,7 @@
package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure,
val inlineSuggestionsRequest: InlineSuggestionsRequest?)

View File

@@ -19,18 +19,27 @@
*/
package com.kunzisoft.keepass.autofill
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -38,8 +47,11 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -47,11 +59,17 @@ object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
intent?.let {
return it.getParcelableExtra(ASSIST_STRUCTURE)
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AutofillComponent(assistStructure,
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
} else {
AutofillComponent(assistStructure, null)
}
}
return null
}
@@ -68,26 +86,10 @@ object AutofillHelper {
return ""
}
internal fun addHeader(responseBuilder: FillResponse.Builder,
packageName: String,
webDomain: String?,
applicationId: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (webDomain != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, webDomain)
})
} else if (applicationId != null) {
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, applicationId)
})
}
}
}
internal fun buildDataset(context: Context,
private fun buildDataset(context: Context,
entryInfo: EntryInfo,
struct: StructureParser.Result): Dataset? {
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, title, entryInfo.icon)
val builder = Dataset.Builder(views)
@@ -100,6 +102,12 @@ object AutofillHelper {
builder.setValue(password, AutofillValue.forText(entryInfo.password))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
return try {
builder.build()
} catch (e: IllegalArgumentException) {
@@ -108,44 +116,120 @@ object AutofillHelper {
}
}
@RequiresApi(Build.VERSION_CODES.R)
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion-1
&& inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(context,
0,
Intent(context, AutofillSettingsActivity::class.java),
0)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title)
setSubtitle(entryInfo.username)
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, entryInfo)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
}
}.build().slice, inlinePresentationSpec, false)
}
return null
}
fun buildResponse(context: Context,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val packageName = context.packageName
parseResult.webDomain?.let { webDomain ->
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
setTextViewText(R.id.autofill_web_domain_text, webDomain)
})
} ?: kotlin.run {
parseResult.applicationId?.let { applicationId ->
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
setTextViewText(R.id.autofill_app_id_text, applicationId)
})
}
}
}
// Add inline suggestion for new IME and dataset
entriesInfo.forEachIndexed { index, entryInfo ->
val inlinePresentation = inlineSuggestionsRequest?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
} else {
null
}
}
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
}
return responseBuilder.build()
}
/**
* Build the Autofill response for one entry
*/
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
}
/**
* Build the Autofill response for many entry
*/
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
if (entriesInfo.isEmpty()) {
activity.setResult(Activity.RESULT_CANCELED)
} else {
var setResultOk = false
activity.intent?.extras?.let { extras ->
if (extras.containsKey(ASSIST_STRUCTURE)) {
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val responseBuilder = FillResponse.Builder()
entriesInfo.forEach {
responseBuilder.addDataset(buildDataset(activity, it, result))
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
responseBuilder.build())
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
StructureParser(structure).parse()?.let { result ->
// New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (inlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
} else {
buildResponse(activity, entriesInfo, result, null)
}
val mReplyIntent = Intent()
Log.d(activity.javaClass.name, "Successed Autofill auth.")
mReplyIntent.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
response)
setResultOk = true
activity.setResult(Activity.RESULT_OK, mReplyIntent)
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
if (!setResultOk) {
Log.w(activity.javaClass.name, "Failed Autofill auth.")
activity.setResult(Activity.RESULT_CANCELED)
}
}
}
@@ -155,10 +239,16 @@ object AutofillHelper {
*/
fun startActivityForAutofillResult(activity: Activity,
intent: Intent,
assistStructure: AssistStructure,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.inlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}
@@ -192,4 +282,11 @@ object AutofillHelper {
}
return presentation
}
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
return createIconFromDatabaseIcon(context,
Database.getInstance().drawFactory,
entryInfo.icon,
ContextCompat.getColor(context, R.color.green))
}
}

View File

@@ -19,37 +19,50 @@
*/
package com.kunzisoft.keepass.autofill
import android.app.PendingIntent
import android.content.Intent
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.*
import android.util.Log
import android.view.autofill.AutofillId
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(api = Build.VERSION_CODES.O)
class KeeAutofillService : AutofillService() {
var applicationIdBlocklist: Set<String>? = null
var webDomainBlocklist: Set<String>? = null
var askToSaveData: Boolean = false
var autofillInlineSuggestionsEnabled: Boolean = false
private var mLock = AtomicBoolean()
override fun onCreate() {
super.onCreate()
getPreferences()
}
private fun getPreferences() {
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed
askToSaveData = PreferencesUtil.askToSaveAutofillData(this)
autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this)
}
override fun onFillRequest(request: FillRequest,
@@ -75,7 +88,16 @@ class KeeAutofillService : AutofillService() {
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
searchInfo.webDomain = webDomainWithoutSubDomain
launchSelection(searchInfo, parseResult, callback)
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
request.inlineSuggestionsRequest
} else {
null
}
launchSelection(searchInfo,
parseResult,
inlineSuggestionsRequest,
callback)
}
}
}
@@ -84,39 +106,40 @@ class KeeAutofillService : AutofillService() {
private fun launchSelection(searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this,
Database.getInstance(),
searchInfo,
{ items ->
val responseBuilder = FillResponse.Builder()
AutofillHelper.addHeader(responseBuilder, packageName,
parseResult.webDomain, parseResult.applicationId)
items.forEach {
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
}
callback.onSuccess(responseBuilder.build())
callback.onSuccess(
AutofillHelper.buildResponse(this,
items, parseResult, inlineSuggestionsRequest)
)
},
{
// Show UI if no search result
showUIForEntrySelection(parseResult, searchInfo, callback)
showUIForEntrySelection(parseResult,
searchInfo, inlineSuggestionsRequest, callback)
},
{
// Show UI if database not open
showUIForEntrySelection(parseResult, searchInfo, callback)
showUIForEntrySelection(parseResult,
searchInfo, inlineSuggestionsRequest, callback)
}
)
}
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response.
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
searchInfo)
searchInfo, inlineSuggestionsRequest)
val responseBuilder = FillResponse.Builder()
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
@@ -149,7 +172,40 @@ class KeeAutofillService : AutofillService() {
)
}
}
// Build response
// Build inline presentation
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.let {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) {
val inlinePresentationSpec = inlinePresentationSpecs[0]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
// Build the content for IME UI
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this,
0,
Intent(this, AutofillSettingsActivity::class.java),
0)
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
}.build().slice, inlinePresentationSpec, false)
}
}
}
// Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
}
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build())
}
@@ -190,6 +246,7 @@ class KeeAutofillService : AutofillService() {
override fun onConnected() {
Log.d(TAG, "onConnected")
getPreferences()
}
override fun onDisconnected() {

View File

@@ -33,7 +33,7 @@ import java.util.*
* Parse AssistStructure and guess username and password fields.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
internal class StructureParser(private val structure: AssistStructure) {
class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null
private var usernameNeeded = true
@@ -274,7 +274,7 @@ internal class StructureParser(private val structure: AssistStructure) {
}
@RequiresApi(api = Build.VERSION_CODES.O)
internal class Result {
class Result {
var applicationId: String? = null
var webDomain: String? = null

View File

@@ -0,0 +1,10 @@
package com.kunzisoft.keepass.biometric
import androidx.annotation.StringRes
import javax.crypto.Cipher
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
@StringRes var promptTitleId: Int,
@StringRes var promptDescriptionId: Int? = null,
var isDeviceCredentialOperation: Boolean,
var isBiometricOperation: Boolean)

View File

@@ -0,0 +1,628 @@
/*
* Copyright 2020 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.biometric
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.*
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
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.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
private var mBuilderListener: BuilderListener? = null
private var mAdvancedUnlockEnabled = false
private var mAutoOpenPromptEnabled = false
private var advancedUnlockManager: AdvancedUnlockManager? = null
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
var databaseFileUri: Uri? = null
private set
/**
* Manage setting to auto open biometric prompt
*/
private var mAutoOpenPrompt: Boolean = false
get() {
return field && mAutoOpenPromptEnabled
}
// Variable to check if the prompt can be open (if the right activity is currently shown)
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
private var allowOpenBiometricPrompt = false
private lateinit var cipherDatabaseAction : CipherDatabaseAction
private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
// Only keep connection when we request a device credential activity
private var keepConnection = false
override fun onAttach(context: Context) {
super.onAttach(context)
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBuilderListener = context as BuilderListener
}
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + BuilderListener::class.java.name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.cloneInContext(contextThemed)
.inflate(R.layout.fragment_advanced_unlock, container, false)
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
return rootView
}
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
if (keepConnection) {
activityResult = ActivityResult(requestCode, resultCode, data)
}
keepConnection = false
}
override fun onResume() {
super.onResume()
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext())
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext())
keepConnection = false
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// biometric menu
if (mAllowAdvancedUnlockMenu)
inflater.inflate(R.menu.advanced_unlock, menu)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEncryptedDatabaseKey()
}
}
return super.onOptionsItemSelected(item)
}
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri
if (databaseUri != null
&& mAdvancedUnlockEnabled) {
activityResult?.let {
if (databaseUri == databaseFileUri) {
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
} else {
disconnect()
}
} ?: run {
this.mAutoOpenPrompt = autoOpenPrompt
connect(databaseUri)
}
} else {
disconnect()
}
activityResult = null
}
}
/**
* Check unlock availability and change the current mode depending of device's state
*/
fun checkUnlockAvailability() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
} else {
selectMode()
}
}
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) {
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
if (AdvancedUnlockManager.isDeviceSecure(requireContext())) {
selectMode()
} else {
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun selectMode() {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
// callback for fingerprint findings
advancedUnlockManager?.advancedUnlockCallback = this
}
// Recheck to change the mode
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (mBuilderListener?.conditionToStoreCredential() == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode(if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initAdvancedUnlockMode()
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotAvailable() {
showViews(false)
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openBiometricSetting() {
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
requireContext().startActivity(Intent(Settings.ACTION_SETTINGS))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initSecurityUpdateRequired() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initNotConfigured() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initKeyManagerNotAvailable() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
openBiometricSetting()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initWaitData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
requireContext().getString(R.string.credential_before_click_advanced_unlock_button))
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
activity?.runOnUiThread {
if (allowOpenBiometricPrompt) {
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
} catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initEncryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
// Set listener to open the biometric dialog and save credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
@RequiresApi(Build.VERSION_CODES.M)
private fun initDecryptData() {
showViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
advancedUnlockManager?.let { unlockHelper ->
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.let {
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
// Set listener to open the biometric dialog and check credential
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
openAdvancedUnlockPrompt(cryptoPrompt)
}
// Auto open the biometric prompt
if (mAutoOpenPrompt) {
mAutoOpenPrompt = false
openAdvancedUnlockPrompt(cryptoPrompt)
}
}
} ?: deleteEncryptedDatabaseKey()
}
} ?: throw IODatabaseException()
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
@Synchronized
fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false
try {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
} catch (e: Exception) {
onGenericException(e)
}
invalidateBiometricMenu()
}
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
activity?.invalidateOptionsMenu()
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun connect(databaseUri: Uri) {
showViews(true)
this.databaseFileUri = databaseUri
cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
override fun onDatabaseCleared() {
deleteEncryptedDatabaseKey()
}
}
cipherDatabaseAction.apply {
reloadPreferences()
cipherDatabaseListener?.let {
registerDatabaseListener(it)
}
}
checkUnlockAvailability()
}
@RequiresApi(Build.VERSION_CODES.M)
fun disconnect(hideViews: Boolean = true,
closePrompt: Boolean = true) {
this.databaseFileUri = null
// Close the biometric prompt
allowOpenBiometricPrompt = false
if (closePrompt)
advancedUnlockManager?.closeBiometricPrompt()
cipherDatabaseListener?.let {
cipherDatabaseAction.unregisterDatabaseListener(it)
}
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
if (hideViews) {
showViews(false)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
allowOpenBiometricPrompt = false
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
checkUnlockAvailability()
}
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationFailed() {
activity?.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onAuthenticationSucceeded() {
activity?.runOnUiThread {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
advancedUnlockManager?.encryptData(credential)
}
AdvancedUnlockNotificationService.startServiceForTimeout(requireContext())
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
cipherDatabase?.encryptedValue?.let { value ->
advancedUnlockManager?.decryptData(value)
} ?: deleteEncryptedDatabaseKey()
}
} ?: run {
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
}
}
}
}
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
databaseFileUri?.let { databaseUri ->
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
}
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
databaseFileUri?.let {
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
override fun onGenericException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showViews(show: Boolean) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.visibility = if (show)
View.VISIBLE
else {
View.GONE
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedTitleView(textId: Int) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setTitle(textId)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun setAdvancedUnlockedMessageView(textId: Int) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
activity?.runOnUiThread {
mAdvancedUnlockInfoView?.message = text
}
}
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
readOnlyEducationPerformed: Boolean,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !readOnlyEducationPerformed) {
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
onEducationViewClick,
onOuterViewClick)
}
} catch (ignored: Exception) {}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
interface BuilderListener {
fun retrieveCredentialForEncryption(): String
fun conditionToStoreCredential(): Boolean
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
}
override fun onPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!keepConnection) {
// If close prompt, bug "user not authenticated in Android R"
disconnect(false)
advancedUnlockManager = null
}
}
super.onPause()
}
override fun onDestroyView() {
mAdvancedUnlockInfoView = null
super.onDestroyView()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
disconnect()
advancedUnlockManager = null
mBuilderListener = null
}
super.onDestroy()
}
override fun onDetach() {
mBuilderListener = null
super.onDetach()
}
companion object {
private val TAG = AdvancedUnlockFragment::class.java.name
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
@@ -31,6 +32,7 @@ import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -44,48 +46,74 @@ import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
@RequiresApi(api = Build.VERSION_CODES.M)
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
private var biometricPrompt: BiometricPrompt? = null
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
private var keyStore: KeyStore? = null
private var keyGenerator: KeyGenerator? = null
private var cipher: Cipher? = null
private var keyguardManager: KeyguardManager? = null
private var cryptoObject: BiometricPrompt.CryptoObject? = null
private var biometricPrompt: BiometricPrompt? = null
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
advancedUnlockCallback?.onAuthenticationSucceeded()
}
override fun onAuthenticationFailed() {
advancedUnlockCallback?.onAuthenticationFailed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
}
}
var advancedUnlockCallback: AdvancedUnlockCallback? = null
private var isKeyManagerInit = false
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
var biometricUnlockCallback: BiometricUnlockCallback? = null
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(context)
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
val isKeyManagerInitialized: Boolean
get() {
if (!isKeyManagerInit) {
biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized"))
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
}
return isKeyManagerInit
}
private fun isBiometricOperation(): Boolean {
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
}
// Since Android 30, device credential is also a biometric operation
private fun isDeviceCredentialOperation(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
private fun isDeviceCredentialBiometricOperation(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable
}
init {
if (allowInitKeyStore(context)) {
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
if (isDeviceSecure(retrieveContext())
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
try {
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
this.cipher = Cipher.getInstance(
BIOMETRIC_KEY_ALGORITHM + "/"
+ BIOMETRIC_BLOCKS_MODES + "/"
+ BIOMETRIC_ENCRYPTION_PADDING)
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
isKeyManagerInit = (keyStore != null
&& keyGenerator != null
&& cipher != null)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize the keystore", e)
isKeyManagerInit = false
biometricUnlockCallback?.onBiometricException(e)
advancedUnlockCallback?.onGenericException(e)
}
} else {
// really not much to do when no fingerprint support found
@@ -103,22 +131,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
keyStore.load(null)
try {
if (!keyStore.containsAlias(BIOMETRIC_KEYSTORE_KEY)) {
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
BIOMETRIC_KEYSTORE_KEY,
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
// Require the user to authenticate with a fingerprint to authorize every use
// of the key
.setUserAuthenticationRequired(true)
// of the key, don't use it for device credential because it's the user authentication
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& deviceCredentialUnlockEnable) {
setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
}
.build())
@@ -126,56 +152,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create a key in keystore", e)
biometricUnlockCallback?.onBiometricException(e)
advancedUnlockCallback?.onGenericException(e)
}
return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey?
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve the key in keystore", e)
biometricUnlockCallback?.onBiometricException(e)
advancedUnlockCallback?.onGenericException(e)
}
return null
}
fun initEncryptData(actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
try {
// TODO if (keyguardManager?.isDeviceSecure == true) {
getSecretKey()?.let { secretKey ->
cipher?.init(Cipher.ENCRYPT_MODE, secretKey)
cipher?.let { cipher ->
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
initBiometricPrompt()
val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(context.getString(R.string.advanced_unlock_prompt_store_credential_title))
setDescription(context.getString(R.string.advanced_unlock_prompt_store_credential_message))
setConfirmationRequired(true)
if (deviceCredentialUnlockEnable) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(context.getString(android.R.string.cancel))
}
}.build()
actionIfCypherInit.invoke(biometricPrompt,
cryptoObject,
promptInfoStoreCredential)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_store_credential_title,
R.string.advanced_unlock_prompt_store_credential_message,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
advancedUnlockCallback?.onGenericException(e)
}
}
@@ -190,57 +206,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// passes updated iv spec on to callback so this can be stored for decryption
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
}
} catch (e: Exception) {
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
Log.e(TAG, "Unable to encrypt data", e)
biometricUnlockCallback?.onBiometricException(exception)
advancedUnlockCallback?.onGenericException(e)
}
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
if (!isKeyManagerInitialized) {
return
}
try {
// TODO if (keyguardManager?.isDeviceSecure == true) {
// important to restore spec here that was used for decryption
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
val spec = IvParameterSpec(iv)
getSecretKey()?.let { secretKey ->
cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec)
cipher?.let { cipher ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
initBiometricPrompt()
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(context.getString(R.string.advanced_unlock_prompt_extract_credential_title))
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
setConfirmationRequired(false)
if (deviceCredentialUnlockEnable) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(context.getString(android.R.string.cancel))
}
}.build()
actionIfCypherInit.invoke(biometricPrompt,
cryptoObject,
promptInfoExtractCredential)
actionIfCypherInit.invoke(
AdvancedUnlockCryptoPrompt(
cipher,
R.string.advanced_unlock_prompt_extract_credential_title,
null,
isDeviceCredentialOperation(), isBiometricOperation())
)
}
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteEntryKey()
deleteKeystoreKey()
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
biometricUnlockCallback?.onBiometricException(e)
advancedUnlockCallback?.onGenericException(e)
}
}
@@ -252,33 +257,73 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
// actual decryption here
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
cipher?.doFinal(encrypted)?.let { decrypted ->
biometricUnlockCallback?.handleDecryptedResult(String(decrypted))
advancedUnlockCallback?.handleDecryptedResult(String(decrypted))
}
} catch (badPaddingException: BadPaddingException) {
Log.e(TAG, "Unable to decrypt data", badPaddingException)
biometricUnlockCallback?.onInvalidKeyException(badPaddingException)
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
} catch (e: Exception) {
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
Log.e(TAG, "Unable to decrypt data", exception)
biometricUnlockCallback?.onBiometricException(exception)
Log.e(TAG, "Unable to decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
}
}
fun deleteEntryKey() {
fun deleteKeystoreKey() {
try {
keyStore?.load(null)
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete entry key in keystore", e)
biometricUnlockCallback?.onBiometricException(e)
advancedUnlockCallback?.onGenericException(e)
}
}
@Suppress("DEPRECATION")
@Synchronized
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
// Init advanced unlock prompt
if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(),
Executors.newSingleThreadExecutor(),
authenticationCallback)
}
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
retrieveContext().getString(descriptionId)
} ?: ""
if (cryptoPrompt.isBiometricOperation) {
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
setTitle(promptTitle)
if (promptDescription.isNotEmpty())
setDescription(promptDescription)
setConfirmationRequired(false)
if (isDeviceCredentialBiometricOperation()) {
setAllowedAuthenticators(DEVICE_CREDENTIAL)
} else {
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
}
}.build()
biometricPrompt?.authenticate(
promptInfoExtractCredential,
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
}
else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
retrieveContext().startActivityForResult(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
REQUEST_DEVICE_CREDENTIAL)
}
}
@Synchronized
fun initBiometricPrompt() {
if (biometricPrompt == null) {
authenticationCallback?.let {
biometricPrompt = BiometricPrompt(context, Executors.newSingleThreadExecutor(), it)
fun onActivityResult(requestCode: Int, resultCode: Int) {
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
if (resultCode == Activity.RESULT_OK) {
advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
}
}
}
@@ -287,25 +332,30 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
biometricPrompt?.cancelAuthentication()
}
interface BiometricUnlockErrorCallback {
interface AdvancedUnlockErrorCallback {
fun onInvalidKeyException(e: Exception)
fun onBiometricException(e: Exception)
fun onGenericException(e: Exception)
}
interface BiometricUnlockCallback : BiometricUnlockErrorCallback {
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
fun onAuthenticationSucceeded()
fun onAuthenticationFailed()
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
fun handleDecryptedResult(decryptedValue: String)
}
companion object {
private val TAG = BiometricUnlockDatabaseHelper::class.java.name
private val TAG = AdvancedUnlockManager::class.java.name
private const val BIOMETRIC_KEYSTORE = "AndroidKeyStore"
private const val BIOMETRIC_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val BIOMETRIC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
@@ -337,11 +387,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
}
@RequiresApi(api = Build.VERSION_CODES.M)
fun allowInitKeyStore(context: Context): Boolean {
val biometricCanAuthenticate = canAuthenticate(context)
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
)
fun isDeviceSecure(context: Context): Boolean {
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
return keyguardManager?.isDeviceSecure ?: false
}
@RequiresApi(api = Build.VERSION_CODES.M)
@@ -365,39 +413,51 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
)
}
@RequiresApi(api = Build.VERSION_CODES.R)
@RequiresApi(api = Build.VERSION_CODES.M)
fun deviceCredentialUnlockSupported(context: Context): Boolean {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
return isDeviceSecure
}
}
return false
}
/**
* Remove entry key in keystore
*/
@RequiresApi(api = Build.VERSION_CODES.M)
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
biometricCallback: BiometricUnlockErrorCallback) {
BiometricUnlockDatabaseHelper(context).apply {
biometricUnlockCallback = object : BiometricUnlockCallback {
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
advancedCallback: AdvancedUnlockErrorCallback) {
AdvancedUnlockManager{ fragmentActivity }.apply {
advancedUnlockCallback = object : AdvancedUnlockCallback {
override fun onAuthenticationSucceeded() {}
override fun onAuthenticationFailed() {}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
override fun handleDecryptedResult(decryptedValue: String) {}
override fun onInvalidKeyException(e: Exception) {
biometricCallback.onInvalidKeyException(e)
advancedCallback.onInvalidKeyException(e)
}
override fun onBiometricException(e: Exception) {
biometricCallback.onBiometricException(e)
override fun onGenericException(e: Exception) {
advancedCallback.onGenericException(e)
}
}
deleteEntryKey()
deleteKeystoreKey()
}
}
}

View File

@@ -1,409 +0,0 @@
/*
* Copyright 2019 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.biometric
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
@RequiresApi(api = Build.VERSION_CODES.M)
class AdvancedUnlockedManager(var context: FragmentActivity,
var databaseFileUri: Uri,
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
private var checkboxPasswordView: CompoundButton?,
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
var passwordView: TextView?,
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
/**
* Manage setting to auto open biometric prompt
*/
private var biometricPromptAutoOpenPreference = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
var isBiometricPromptAutoOpenEnable: Boolean = false
get() {
return field && biometricPromptAutoOpenPreference
}
// Variable to check if the prompt can be open (if the right activity is currently shown)
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
private var allowOpenBiometricPrompt = false
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
init {
// Add a check listener to change fingerprint mode
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
checkBiometricAvailability()
// Add old listener to enable the button, only be call here because of onCheckedChange bug
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
}
}
/**
* Check biometric availability and change the current mode depending of device's state
*/
fun checkBiometricAvailability() {
if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
advancedUnlockInfoView?.setIconResource(R.drawable.bolt)
} else if (PreferencesUtil.isBiometricUnlockEnable(context)) {
advancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
}
// biometric not supported (by API level or hardware) so keep option hidden
// or manually disable
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
allowOpenBiometricPrompt = true
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
} else {
// biometric is available but not configured, show icon but in disabled state with some information
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
toggleMode(Mode.BIOMETRIC_NOT_CONFIGURED)
} else {
// Check if fingerprint well init (be called the first time the fingerprint is configured
// and the activity still active)
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
// callback for fingerprint findings
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
}
// Recheck to change the mode
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
} else {
if (checkboxPasswordView?.isChecked == true) {
// listen for encryption
toggleMode(Mode.STORE_CREDENTIAL)
} else {
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
// biometric available but no stored password found yet for this DB so show info don't listen
toggleMode(if (containsCipher) {
// listen for decryption
Mode.EXTRACT_CREDENTIAL
} else {
// wait for typing
Mode.WAIT_CREDENTIAL
})
}
}
}
}
}
}
private fun toggleMode(newBiometricMode: Mode) {
if (newBiometricMode != biometricMode) {
biometricMode = newBiometricMode
initAdvancedUnlockMode()
}
}
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence) {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
setAdvancedUnlockedMessageView(errString.toString())
}
}
override fun onAuthenticationFailed() {
context.runOnUiThread {
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
context.runOnUiThread {
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> {
}
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
}
Mode.BIOMETRIC_NOT_CONFIGURED -> {
}
Mode.KEY_MANAGER_UNAVAILABLE -> {
}
Mode.WAIT_CREDENTIAL -> {
}
Mode.STORE_CREDENTIAL -> {
// newly store the entered password in encrypted way
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
}
Mode.EXTRACT_CREDENTIAL -> {
// retrieve the encrypted value from preferences
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.encryptedValue?.let { value ->
biometricUnlockDatabaseHelper?.decryptData(value)
}
}
}
}
}
}
}
private fun initNotAvailable() {
showFingerPrintViews(false)
advancedUnlockInfoView?.setIconViewClickListener(false, null)
}
private fun initSecurityUpdateRequired() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initNotConfigured() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.configure_biometric)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initKeyManagerNotAvailable() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
advancedUnlockInfoView?.setIconViewClickListener(false) {
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}
}
private fun initWaitData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
setAdvancedUnlockedMessageView("")
advancedUnlockInfoView?.setIconViewClickListener(false) {
biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
context.getString(R.string.credential_before_click_advanced_unlock_button))
}
}
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
cryptoObject: BiometricPrompt.CryptoObject?,
promptInfo: BiometricPrompt.PromptInfo) {
context.runOnUiThread {
if (allowOpenBiometricPrompt) {
if (biometricPrompt != null) {
if (cryptoObject != null) {
biometricPrompt.authenticate(promptInfo, cryptoObject)
} else {
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
}
} else {
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
}
}
}
}
private fun initEncryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
setAdvancedUnlockedMessageView("")
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
// Set listener to open the biometric dialog and save credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
}
}
private fun initDecryptData() {
showFingerPrintViews(true)
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
setAdvancedUnlockedMessageView("")
if (biometricUnlockDatabaseHelper != null) {
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
it?.specParameters?.let { specs ->
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
// Set listener to open the biometric dialog and check credential
advancedUnlockInfoView?.setIconViewClickListener { _ ->
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
// Auto open the biometric prompt
if (isBiometricPromptAutoOpenEnable) {
isBiometricPromptAutoOpenEnable = false
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
}
}
}
}
}
}
@Synchronized
fun initAdvancedUnlockMode() {
mAllowAdvancedUnlockMenu = false
when (biometricMode) {
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
Mode.WAIT_CREDENTIAL -> initWaitData()
Mode.STORE_CREDENTIAL -> initEncryptData()
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
}
invalidateBiometricMenu()
}
private fun invalidateBiometricMenu() {
// Show fingerprint key deletion
if (!mAddBiometricMenuInProgress) {
mAddBiometricMenuInProgress = true
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
mAllowAdvancedUnlockMenu = containsCipher
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
mAddBiometricMenuInProgress = false
context.invalidateOptionsMenu()
}
}
}
fun destroy() {
// Close the biometric prompt
allowOpenBiometricPrompt = false
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
// Restore the checked listener
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
}
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
if (mAllowAdvancedUnlockMenu)
menuInflater.inflate(R.menu.advanced_unlock, menu)
}
fun deleteEntryKey() {
allowOpenBiometricPrompt = false
advancedUnlockInfoView?.setIconViewClickListener(false, null)
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
biometricUnlockDatabaseHelper?.deleteEntryKey()
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
checkBiometricAvailability()
}
}
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
}
override fun handleDecryptedResult(decryptedValue: String) {
// Load database directly with password retrieve
loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue)
}
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
override fun onBiometricException(e: Exception) {
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
setAdvancedUnlockedMessageView(errorMessage)
}
private fun showFingerPrintViews(show: Boolean) {
context.runOnUiThread {
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
}
}
private fun setAdvancedUnlockedTitleView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setTitle(textId)
}
}
private fun setAdvancedUnlockedMessageView(textId: Int) {
context.runOnUiThread {
advancedUnlockInfoView?.setMessage(textId)
}
}
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
context.runOnUiThread {
advancedUnlockInfoView?.message = text
}
}
enum class Mode {
BIOMETRIC_UNAVAILABLE,
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
BIOMETRIC_NOT_CONFIGURED,
KEY_MANAGER_UNAVAILABLE,
WAIT_CREDENTIAL,
STORE_CREDENTIAL,
EXTRACT_CREDENTIAL
}
companion object {
private val TAG = AdvancedUnlockedManager::class.java.name
}
}

View File

@@ -29,11 +29,12 @@ object StreamCipherFactory {
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher? {
@Throws(Exception::class)
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher {
return when {
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
else -> null
else -> throw Exception("Invalid random cipher")
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.crypto.keyDerivation
import android.content.res.Resources
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.stream.bytes16ToUuid
import com.kunzisoft.keepass.utils.UnsignedInt
@@ -27,7 +28,11 @@ import java.io.IOException
import java.security.SecureRandom
import java.util.*
class Argon2Kdf internal constructor() : KdfEngine() {
class Argon2Kdf(private val type: Type) : KdfEngine() {
init {
uuid = type.CIPHER_UUID
}
override val defaultParameters: KdfParameters
get() {
@@ -45,12 +50,8 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val defaultKeyRounds: Long
get() = DEFAULT_ITERATIONS
init {
uuid = CIPHER_UUID
}
override fun getName(resources: Resources): String {
return resources.getString(R.string.kdf_Argon2)
return resources.getString(type.nameId)
}
@Throws(IOException::class)
@@ -72,7 +73,9 @@ class Argon2Kdf internal constructor() : KdfEngine() {
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
return Argon2Native.transformKey(masterKey,
return Argon2Native.transformKey(
type,
masterKey,
salt,
parallelism,
memory,
@@ -141,9 +144,8 @@ class Argon2Kdf internal constructor() : KdfEngine() {
override val maxParallelism: Long
get() = MAX_PARALLELISM
companion object {
val CIPHER_UUID: UUID = bytes16ToUuid(
enum class Type(val CIPHER_UUID: UUID, @StringRes val nameId: Int) {
ARGON2_D(bytes16ToUuid(
byteArrayOf(0xEF.toByte(),
0x63.toByte(),
0x6D.toByte(),
@@ -159,7 +161,27 @@ class Argon2Kdf internal constructor() : KdfEngine() {
0x03.toByte(),
0xE3.toByte(),
0x0A.toByte(),
0x0C.toByte()))
0x0C.toByte())), R.string.kdf_Argon2d),
ARGON2_ID(bytes16ToUuid(
byteArrayOf(0x9E.toByte(),
0x29.toByte(),
0x8B.toByte(),
0x19.toByte(),
0x56.toByte(),
0xDB.toByte(),
0x47.toByte(),
0x73.toByte(),
0xB2.toByte(),
0x3D.toByte(),
0xFC.toByte(),
0x3E.toByte(),
0xC6.toByte(),
0xF0.toByte(),
0xA1.toByte(),
0xE6.toByte())), R.string.kdf_Argon2id);
}
companion object {
private const val PARAM_SALT = "S" // byte[]
private const val PARAM_PARALLELISM = "P" // UInt32

View File

@@ -26,12 +26,29 @@ import java.io.IOException;
public class Argon2Native {
public static byte[] transformKey(byte[] password, byte[] salt, UnsignedInt parallelism,
enum CType {
ARGON2_D(0),
ARGON2_I(1),
ARGON2_ID(2);
int cValue = 0;
CType(int i) {
cValue = i;
}
}
public static byte[] transformKey(Argon2Kdf.Type type, byte[] password, byte[] salt, UnsignedInt parallelism,
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
byte[] associatedData, UnsignedInt version) throws IOException {
NativeLib.INSTANCE.init();
CType cType = CType.ARGON2_D;
if (type.equals(Argon2Kdf.Type.ARGON2_ID))
cType = CType.ARGON2_ID;
return nTransformMasterKey(
cType.cValue,
password,
salt,
parallelism.toKotlinInt(),
@@ -42,7 +59,7 @@ public class Argon2Native {
version.toKotlinInt());
}
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
private static native byte[] nTransformMasterKey(int type, byte[] password, byte[] salt, int parallelism,
int memory, int iterations, byte[] secretKey,
byte[] associatedData, int version) throws IOException;
}

View File

@@ -21,5 +21,6 @@ package com.kunzisoft.keepass.crypto.keyDerivation
object KdfFactory {
var aesKdf = AesKdf()
var argon2Kdf = Argon2Kdf()
var argon2dKdf = Argon2Kdf(Argon2Kdf.Type.ARGON2_D)
var argon2idKdf = Argon2Kdf(Argon2Kdf.Type.ARGON2_ID)
}

View File

@@ -24,39 +24,26 @@ 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
open class AssignPasswordInDatabaseRunnable (
context: Context,
database: Database,
protected val mDatabaseUri: Uri,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?)
protected val mMainCredential: MainCredential)
: SaveDatabaseRunnable(context, database, true) {
private var mMasterPassword: String? = null
protected var mKeyFileUri: Uri? = null
private var mBackupKey: ByteArray? = null
init {
if (withMasterPassword)
this.mMasterPassword = masterPassword
if (withKeyFile)
this.mKeyFileUri = keyFile
}
override fun onStartRun() {
// Set key
try {
// TODO move master key methods
mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
database.retrieveMasterKey(mMasterPassword, uriInputStream)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) {
erase(mBackupKey)
setError(e)

View File

@@ -24,21 +24,18 @@ 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.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
class CreateDatabaseRunnable(context: Context,
private val mDatabase: Database,
databaseUri: Uri,
private val databaseName: String,
private val rootName: String,
withMasterPassword: Boolean,
masterPassword: String?,
withKeyFile: Boolean,
keyFile: Uri?,
mainCredential: MainCredential,
private val createDatabaseResult: ((Result) -> Unit)?)
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
override fun onStartRun() {
try {
@@ -47,7 +44,7 @@ class CreateDatabaseRunnable(context: Context,
createData(mDatabaseUri, databaseName, rootName)
}
} catch (e: Exception) {
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
setError(e)
}
@@ -62,7 +59,7 @@ class CreateDatabaseRunnable(context: Context,
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
}
// Register the current time to init the lock timer

View File

@@ -25,19 +25,17 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.closeDatabase
class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val mUri: Uri,
private val mPass: String?,
private val mKey: Uri?,
private val mMainCredential: MainCredential,
private val mReadonly: Boolean,
private val mCipherEntity: CipherDatabaseEntity?,
private val mFixDuplicateUUID: Boolean,
@@ -47,21 +45,19 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onStartRun() {
// Clear before we load
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
override fun onActionRun() {
try {
mDatabase.loadData(mUri, mPass, mKey,
mDatabase.loadData(mUri,
mMainCredential,
mReadonly,
context.contentResolver,
UriUtil.getBinaryDir(context),
mFixDuplicateUUID,
progressTaskUpdater)
}
catch (e: DuplicateUuidDatabaseException) {
setError(e)
}
catch (e: LoadDatabaseException) {
setError(e)
}
@@ -71,7 +67,7 @@ class LoadDatabaseRunnable(private val context: Context,
if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
}
// Register the biometric
@@ -83,7 +79,7 @@ class LoadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
}

View File

@@ -26,6 +26,8 @@ import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Entry
@@ -35,34 +37,37 @@ 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.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
@@ -84,6 +89,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
private var serviceConnection: ServiceConnection? = null
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
@@ -101,6 +107,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
}
}
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
override fun validateDatabaseChanged() {
mBinder?.getService()?.saveDatabaseInfo()
}
}
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo) {
if (databaseChangedDialogFragment == null) {
databaseChangedDialogFragment = activity.supportFragmentManager
.findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment?
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
}
if (progressTaskDialogFragment == null) {
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo)
databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener
databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG)
}
}
}
private fun startDialog(titleId: Int? = null,
messageId: Int? = null,
warningId: Int? = null) {
@@ -140,11 +168,14 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addActionTaskListener(actionTaskListener)
addDatabaseFileInfoListener(databaseInfoListener)
getService().checkAction()
getService().checkDatabaseInfo()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
}
@@ -206,6 +237,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
fun unregisterProgressTask() {
stopDialog()
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeActionTaskListener(actionTaskListener)
mBinder = null
@@ -233,30 +265,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
*/
fun startDatabaseCreate(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
mainCredential: MainCredential) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_CREATE_TASK)
}
fun startDatabaseLoad(databaseUri: Uri,
masterPassword: String?,
keyFile: Uri?,
mainCredential: MainCredential,
readOnly: Boolean,
cipherEntity: CipherDatabaseEntity?,
fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -264,18 +288,19 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
, ACTION_DATABASE_LOAD_TASK)
}
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
}
, ACTION_DATABASE_RELOAD_TASK)
}
fun startDatabaseAssignPassword(databaseUri: Uri,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
mainCredential: MainCredential) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
}
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2019 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.action
import android.content.Context
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.UriUtil
class ReloadDatabaseRunnable(private val context: Context,
private val mDatabase: Database,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
override fun onStartRun() {
// Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context))
}
override fun onActionRun() {
try {
mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context),
progressTaskUpdater)
} catch (e: LoadDatabaseException) {
setError(e)
}
if (result.isSuccess) {
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
}
}
override fun onFinishRun() {
mLoadDatabaseResult?.invoke(result)
}
}

View File

@@ -31,10 +31,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
@@ -44,6 +41,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.stream.readBytes4ToUInt
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder
@@ -330,29 +328,11 @@ class Database {
}
@Throws(LoadDatabaseException::class)
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
this.fileUri = uri
isReadOnly = readOnly
if (uri.scheme == "file") {
val file = File(uri.path!!)
isReadOnly = !file.canWrite()
}
// Pass KeyFile Uri as InputStreams
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
openDatabaseKDB: (InputStream) -> DatabaseKDB,
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
var databaseInputStream: InputStream? = null
var keyFileInputStream: InputStream? = null
try {
// Get keyFile inputStream
keyfile?.let {
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
}
// Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
?: throw IOException("Database input stream cannot be retrieve")
@@ -374,22 +354,10 @@ class Database {
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB(
cacheDirectory,
fixDuplicateUUID)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX(
cacheDirectory,
fixDuplicateUUID)
.openDatabase(databaseInputStream,
password,
keyFileInputStream,
progressTaskUpdater))
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
// Header not recognized
else -> throw SignatureDatabaseException()
@@ -397,14 +365,102 @@ class Database {
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,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
// Save database URI
this.fileUri = uri
// 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,
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory)
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
progressTaskUpdater,
fixDuplicateUUID)
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory)
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
progressTaskUpdater,
fixDuplicateUUID)
}
)
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e)
throw FileNotFoundDatabaseException()
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
keyFileInputStream?.close()
databaseInputStream?.close()
}
}
@Throws(LoadDatabaseException::class)
fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File,
progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI
try {
fileUri?.let { oldDatabaseUri ->
readDatabaseStream(contentResolver, oldDatabaseUri,
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory)
.openDatabase(databaseInputStream,
masterKey,
progressTaskUpdater)
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory)
.openDatabase(databaseInputStream,
masterKey,
progressTaskUpdater)
}
)
} ?: run {
Log.e(TAG, "Database URI is null, database cannot be reloaded")
throw IODatabaseException()
}
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e)
throw FileNotFoundDatabaseException()
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
}
}
@@ -426,7 +482,7 @@ class Database {
max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
searchInfoString, SearchParameters().apply {
searchInTitles = false
searchInTitles = true
searchInUserNames = false
searchInPasswords = false
searchInUrls = true
@@ -531,7 +587,7 @@ class Database {
this.fileUri = uri
}
fun closeAndClear(filesDirectory: File? = null) {
fun clear(filesDirectory: File? = null) {
drawFactory.clearCache()
// Delete the cache of the database if present
mDatabaseKDB?.clearCache()
@@ -544,7 +600,10 @@ class Database {
} catch (e: Exception) {
Log.e(TAG, "Unable to clear the directory cache.", e)
}
}
fun clearAndClose(filesDirectory: File? = null) {
clear(filesDirectory)
this.mDatabaseKDB = null
this.mDatabaseKDBX = null
this.fileUri = null
@@ -562,7 +621,9 @@ class Database {
}
}
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
val password = mainCredential.masterPassword
val containsKeyFile = mainCredential.keyFileUri != null
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false

View File

@@ -426,6 +426,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.icon = icon
entryInfo.username = username
entryInfo.password = password
entryInfo.creationTime = creationTime
entryInfo.modificationTime = lastModificationTime
entryInfo.expires = expires
entryInfo.expiryTime = expiryTime
entryInfo.url = url
@@ -456,6 +458,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
icon = newEntryInfo.icon
username = newEntryInfo.username
password = newEntryInfo.password
// Update date time, creation time stay as is
lastModificationTime = DateInstant()
lastAccessTime = DateInstant()
expires = newEntryInfo.expires
expiryTime = newEntryInfo.expiryTime
url = newEntryInfo.url
@@ -464,9 +469,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
database?.binaryPool?.let { binaryPool ->
addAttachments(binaryPool, newEntryInfo.attachments)
}
// Update date time
lastAccessTime = DateInstant()
lastModificationTime = DateInstant()
database?.stopManageEntry(this)
}

View File

@@ -163,10 +163,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
finalKey = messageDigest.digest()
}
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
override fun createGroup(): GroupKDB {
return GroupKDB()
}

View File

@@ -43,10 +43,12 @@ import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import org.w3c.dom.Text
import java.io.File
import java.io.IOException
import java.io.InputStream
@@ -113,7 +115,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
init {
kdfList.add(KdfFactory.aesKdf)
kdfList.add(KdfFactory.argon2Kdf)
kdfList.add(KdfFactory.argon2dKdf)
kdfList.add(KdfFactory.argon2idKdf)
}
constructor()
@@ -123,6 +126,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
*/
constructor(databaseName: String, rootName: String) {
name = databaseName
kdbxVersion = FILE_VERSION_32_3
val group = createGroup().apply {
title = rootName
icon = iconFactory.folderIcon
@@ -179,7 +183,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
when (oldCompression) {
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {}
CompressionAlgorithm.None -> {
}
CompressionAlgorithm.GZip -> {
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
@@ -197,7 +202,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
CompressionAlgorithm.None -> {
decompressAllBinaries()
}
CompressionAlgorithm.GZip -> {}
CompressionAlgorithm.GZip -> {
}
}
}
}
@@ -377,36 +383,82 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.e(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)", e)
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
if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) {
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
val children = docElement.childNodes
if (children.length < 2) {
if (keyFileChildNodes.length < 2)
return null
}
for (i in 0 until children.length) {
val child = children.item(i)
if (child.nodeName.equals(KeyElementName, ignoreCase = true)) {
val keyChildren = child.childNodes
for (j in 0 until keyChildren.length) {
val keyChild = keyChildren.item(j)
if (keyChild.nodeName.equals(KeyDataElementName, ignoreCase = true)) {
val children2 = keyChild.childNodes
for (k in 0 until children2.length) {
val text = children2.item(k)
if (text.nodeType == Node.TEXT_NODE) {
val txt = text as Text
return Base64.decode(txt.nodeValue, BASE_64_FLAG)
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
}
}
}
}
}
}
@@ -416,10 +468,26 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
val digest: MessageDigest?
var success = false
try {
digest = MessageDigest.getInstance("SHA-256")
digest?.reset()
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4)
.toHexString()
success = dataDigest == hash
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
return success
}
override fun newGroupId(): NodeIdUUID {
var newId: NodeIdUUID
do {
@@ -633,11 +701,12 @@ 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 RootElementName = "KeyFile"
//private const val MetaElementName = "Meta";
//private const val VersionElementName = "Version";
private const val KeyElementName = "Key"
private const val KeyDataElementName = "Data"
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

@@ -27,7 +27,11 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import java.io.*
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.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
@@ -124,42 +128,35 @@ abstract class DatabaseVersioned<
@Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyByteArrayOutputStream = ByteArrayOutputStream()
keyInputStream.copyTo(keyByteArrayOutputStream)
val keyData = keyByteArrayOutputStream.toByteArray()
val keyData = keyInputStream.readBytes()
val keyByteArrayInputStream = ByteArrayInputStream(keyData)
val key = loadXmlKeyFile(keyByteArrayInputStream)
if (key != null) {
return key
// Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
when (keyData.size.toLong()) {
32L -> return keyData
64L -> try {
return hexStringToByteArray(String(keyData))
} catch (e: IndexOutOfBoundsException) {
// 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
}
}
val messageDigest: MessageDigest
// Hash file as binary data
try {
messageDigest = MessageDigest.getInstance("SHA-256")
return MessageDigest.getInstance("SHA-256").digest(keyData)
} catch (e: NoSuchAlgorithmException) {
throw IOException("SHA-256 not supported")
}
try {
messageDigest.update(keyData)
} catch (e: Exception) {
println(e.toString())
}
return messageDigest.digest()
}
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null
}
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (password == null && !containsKeyFile)
@@ -391,16 +388,5 @@ abstract class DatabaseVersioned<
private const val TAG = "DatabaseVersioned"
val UUID_ZERO = UUID(0, 0)
fun hexStringToByteArray(s: String): ByteArray {
val len = s.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
i += 2
}
return data
}
}
}

View File

@@ -43,20 +43,12 @@ abstract class DatabaseException : Exception {
}
open class LoadDatabaseException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(throwable: Throwable) : super(throwable)
}
class ArcFourDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_arc4
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.file_not_found_content
@@ -67,7 +59,6 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
class InvalidAlgorithmDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_algorithm
constructor() : super()
constructor(exception: Throwable) : super(exception)
}

View File

@@ -41,6 +41,13 @@ abstract class DatabaseInput<PwDb : DatabaseVersioned<*, *, *, *>>
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): PwDb
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): PwDb
}

View File

@@ -45,8 +45,7 @@ import javax.crypto.spec.SecretKeySpec
/**
* Load a KDB database file.
*/
class DatabaseInputKDB(cacheDirectory: File,
private val fixDuplicateUUID: Boolean = false)
class DatabaseInputKDB(cacheDirectory: File)
: DatabaseInput<DatabaseKDB>(cacheDirectory) {
private lateinit var mDatabaseToOpen: DatabaseKDB
@@ -55,7 +54,28 @@ class DatabaseInputKDB(cacheDirectory: File,
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabaseToOpen.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try {
// Load entire file, most of it's encrypted.
@@ -84,7 +104,7 @@ class DatabaseInputKDB(cacheDirectory: File,
mDatabaseToOpen = DatabaseKDB()
mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID
mDatabaseToOpen.retrieveMasterKey(password, keyInputStream)
assignMasterKey?.invoke()
// Select algorithm
when {

View File

@@ -25,9 +25,10 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.CipherFactory
import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
@@ -37,7 +38,6 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -63,8 +63,7 @@ import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.math.min
class DatabaseInputKDBX(cacheDirectory: File,
private val fixDuplicateUUID: Boolean = false)
class DatabaseInputKDBX(cacheDirectory: File)
: DatabaseInput<DatabaseKDBX>(cacheDirectory) {
private var randomStream: StreamCipher? = null
@@ -98,12 +97,30 @@ class DatabaseInputKDBX(cacheDirectory: File,
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyInputStream: InputStream?,
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.retrieveMasterKey(password, keyInputStream)
}
}
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.masterKey = masterKey
}
}
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try {
// TODO performance
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
mDatabase = DatabaseKDBX()
mDatabase.changeDuplicateId = fixDuplicateUUID
@@ -116,9 +133,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header
mDatabase.retrieveMasterKey(password, keyInputStream)
assignMasterKey?.invoke()
mDatabase.makeFinalKey(header.masterSeed)
// TODO performance
progressTaskUpdater?.updateMessage(R.string.decrypting_db)
val engine: CipherEngine
@@ -185,10 +201,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
loadInnerHeader(inputStreamXml, header)
}
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
if (randomStream == null) {
throw ArcFourDatabaseException()
try {
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
} catch (e: Exception) {
throw LoadDatabaseException(e)
}
readDocumentStreamed(createPullParser(inputStreamXml))
@@ -436,8 +452,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
val strData = readString(xpp)
if (strData.isNotEmpty()) {
customIconData = Base64.decode(strData, BASE_64_FLAG)
} else {
assert(false)
}
} else {
readUnknown(xpp)
@@ -958,7 +972,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
// Create empty binary if not retrieved in pool
if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
compression = false, protection = true, binaryPoolId = id)
compression = false, protection = false, binaryPoolId = id)
}
return binaryRetrieve
}
@@ -1024,29 +1038,20 @@ class DatabaseInputKDBX(cacheDirectory: File,
return xpp.safeNextText()
}
@Throws(XmlPullParserException::class, IOException::class)
private fun readBase64String(xpp: XmlPullParser): ByteArray {
//readNextNode = false;
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
val plainText = ByteArray(data.size)
randomStream?.processBytes(data, 0, data.size, plainText, 0)
return plainText
}
return ByteArray(0)
}
@Throws(XmlPullParserException::class, IOException::class)
private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
//(xpp.getEventType() == XmlPullParser.START_TAG);
if (xpp.attributeCount > 0) {
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
return readBase64String(xpp)
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
val plainText = ByteArray(data.size)
randomStream?.processBytes(data, 0, data.size, plainText, 0)
return plainText
}
return ByteArray(0)
}
}
return null
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2019 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.file.output
open class DatabaseHeaderOutput {
var hashOfHeader: ByteArray? = null
protected set
}

View File

@@ -40,13 +40,16 @@ import javax.crypto.spec.SecretKeySpec
class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class)
constructor(private val databaseKDBX: DatabaseKDBX,
private val header: DatabaseHeaderKDBX,
outputStream: OutputStream) : DatabaseHeaderOutput() {
outputStream: OutputStream) {
private val los: LittleEndianDataOutputStream
private val mos: MacOutputStream
private val dos: DigestOutputStream
lateinit var headerHmac: ByteArray
var hashOfHeader: ByteArray? = null
private set
init {
val md: MessageDigest

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDroid 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.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.file.output
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
import com.kunzisoft.keepass.stream.readBytes
import com.kunzisoft.keepass.utils.UnsignedInt
import java.io.IOException
import java.io.OutputStream
import kotlin.experimental.or
class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
private val header: DatabaseHeaderKDBX,
outputStream: OutputStream) {
private val dataOutputStream: LittleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream)
@Throws(IOException::class)
fun output() {
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
dataOutputStream.writeInt(4)
if (header.innerRandomStream == null)
throw IOException("Can't write innerRandomStream")
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
val streamKeySize = header.innerRandomStreamKey.size
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
// Force decompression to add binary in header
protectedBinary.decompress()
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
}
}
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
dataOutputStream.writeInt(0)
}
}

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 mOS: OutputStream) {
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
@Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom {

View File

@@ -63,7 +63,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
// and remove any orphaned nodes that are no longer part of the tree hierarchy
sortGroupsForOutput()
val header = outputHeader(mOS)
val header = outputHeader(mOutputStream)
val finalKey = getFinalKey(header)
@@ -85,7 +85,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
cipher.init(Cipher.ENCRYPT_MODE,
SecretKeySpec(finalKey, "AES"),
IvParameterSpec(header.encryptionIV))
val cos = CipherOutputStream(mOS, cipher)
val cos = CipherOutputStream(mOutputStream, cipher)
val bos = BufferedOutputStream(cos)
outputPlanGroupAndEntries(bos)
bos.flush()

View File

@@ -38,7 +38,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -47,6 +46,7 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.database.file.DateKDBXUtil
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.utils.UnsignedInt
import org.bouncycastle.crypto.StreamCipher
import org.joda.time.DateTime
import org.xmlpull.v1.XmlSerializer
@@ -58,6 +58,7 @@ import java.util.*
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import kotlin.experimental.or
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
@@ -81,20 +82,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
throw DatabaseOutputException("No such cipher", e)
}
header = outputHeader(mOS)
header = outputHeader(mOutputStream)
val osPlain: OutputStream
osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val cos = attachStreamEncryptor(header!!, mOS)
val cos = attachStreamEncryptor(header!!, mOutputStream)
cos.write(header!!.streamStartBytes)
HashedBlockOutputStream(cos)
} else {
mOS.write(hashOfHeader!!)
mOS.write(headerHmac!!)
mOutputStream.write(hashOfHeader!!)
mOutputStream.write(headerHmac!!)
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey!!))
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
}
val osXml: OutputStream
@@ -105,8 +105,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml)
ihOut.output()
outputInnerHeader(mDatabaseKDBX, header!!, osXml)
}
outputDatabase(osXml)
@@ -122,6 +121,49 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
}
@Throws(IOException::class)
private fun outputInnerHeader(database: DatabaseKDBX,
header: DatabaseHeaderKDBX,
outputStream: OutputStream) {
val dataOutputStream = LittleEndianDataOutputStream(outputStream)
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
dataOutputStream.writeInt(4)
if (header.innerRandomStream == null)
throw IOException("Can't write innerRandomStream")
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
val streamKeySize = header.innerRandomStreamKey.size
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
// Force decompression to add binary in header
protectedBinary.decompress()
// Write type binary
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
// Write size
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
// Write protected flag
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
dataOutputStream.writeByte(flag)
protectedBinary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
}
}
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
dataOutputStream.writeInt(0)
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun outputDatabase(outputStream: OutputStream) {
@@ -282,9 +324,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
random.nextBytes(header.innerRandomStreamKey)
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
if (randomStream == null) {
throw DatabaseOutputException("Invalid random cipher")
try {
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
} catch (e: Exception) {
throw DatabaseOutputException(e)
}
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
@@ -420,41 +463,56 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
}
/*
// Normally used by a single entry but obsolete because binaries are in meta tag with kdbx3.1-
// or in file header with kdbx4
// binary.isProtected attribute is not used to create the XML
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeBinary(binary : BinaryAttachment) {
val binaryLength = binary.length()
if (binaryLength > 0) {
private fun writeEntryBinary(binary : BinaryAttachment) {
if (binary.length() > 0) {
if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
val encoded = ByteArray(buffer.size)
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray()
xml.text(charArray, 0, charArray.size)
binary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
val encoded = ByteArray(buffer.size)
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
}
}
} else {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Write the XML
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
xml.text(charArray, 0, charArray.size)
binary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
}
}
}
}
}
*/
// Only uses with kdbx3.1 to write binaries in meta tag
// With kdbx4, don't use this method because binaries are in header file
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
// Use indexes because necessarily (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
writeBinary(keyBinary.binary)
val binary = keyBinary.binary
if (binary.length() > 0) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Write the XML
binary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
}
}
}
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
@@ -523,17 +581,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (protect) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
val data = value.toString().toByteArray(charset("UTF-8"))
val valLength = data.size
if (valLength > 0) {
val encoded = ByteArray(valLength)
randomStream!!.processBytes(data, 0, valLength, encoded, 0)
val data = value.toString().toByteArray()
val dataLength = data.size
if (data.isNotEmpty()) {
val encoded = ByteArray(dataLength)
randomStream!!.processBytes(data, 0, dataLength, encoded, 0)
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
}
} else {
xml.text(safeXmlString(value.toString()))
xml.text(value.toString())
}
xml.endTag(null, DatabaseKDBXXML.ElemValue)
@@ -662,17 +718,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
if (text.isEmpty()) {
return text
}
val stringBuilder = StringBuilder()
var ch: Char
var character: Char
for (element in text) {
ch = element
character = element
val hexChar = character.toInt()
if (
ch.toInt() in 0x20..0xD7FF ||
ch.toInt() == 0x9 || ch.toInt() == 0xA || ch.toInt() == 0xD ||
ch.toInt() in 0xE000..0xFFFD
hexChar in 0x20..0xD7FF ||
hexChar == 0x9 ||
hexChar == 0xA ||
hexChar == 0xD ||
hexChar in 0xE000..0xFFFD
) {
stringBuilder.append(ch)
stringBuilder.append(character)
}
}
return stringBuilder.toString()

View File

@@ -86,8 +86,8 @@ class PasswordActivityEducation(activity: Activity)
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationBiometricPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_biometric_title),
activity.getString(R.string.education_biometric_summary))
activity.getString(R.string.education_advanced_unlock_title),
activity.getString(R.string.education_advanced_unlock_summary))
.textColorInt(Color.WHITE)
.tintTarget(false)
.cancelable(true),

View File

@@ -26,9 +26,12 @@ import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Log
import android.widget.ImageView
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.widget.ImageViewCompat
@@ -87,6 +90,22 @@ class IconDrawableFactory {
remoteViews.setImageViewBitmap(imageId, bitmap)
}
/**
* Utility method to assign a drawable to a icon and tint it
*/
@RequiresApi(Build.VERSION_CODES.M)
fun assignDrawableToIcon(superDrawable: SuperDrawable,
tintColor: Int = Color.BLACK): Icon {
val bitmap = superDrawable.drawable.toBitmap()
// Tint bitmap if it's not a custom icon
if (superDrawable.tintable && bitmap.isMutable) {
Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
})
}
return Icon.createWithBitmap(bitmap)
}
/**
* Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed
*/
@@ -309,3 +328,22 @@ fun RemoteViews.assignDatabaseIcon(context: Context,
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun createIconFromDatabaseIcon(context: Context,
iconFactory: IconDrawableFactory,
icon: IconImage,
tintColor: Int = Color.BLACK): Icon? {
try {
return iconFactory.assignDrawableToIcon(
iconFactory.getIconSuperDrawable(context,
icon,
24,
true,
tintColor),
tintColor)
} catch (e: Exception) {
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
}
return null
}

View File

@@ -41,7 +41,8 @@ import com.kunzisoft.keepass.adapters.FieldsAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.*
@@ -243,6 +244,14 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
if (entryInfoKey != null) {
currentInputConnection.commitText(entryInfoKey!!.password, 1)
}
val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false
actionGoAutomatically(!otpFieldExists)
}
KEY_OTP -> {
if (entryInfoKey != null) {
currentInputConnection.commitText(
entryInfoKey!!.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
}
actionGoAutomatically()
}
KEY_URL -> {
@@ -254,7 +263,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
KEY_FIELDS -> {
if (entryInfoKey != null) {
fieldsAdapter?.apply {
setFields(entryInfoKey!!.customFields)
setFields(entryInfoKey!!.customFields.filter { it.name != OTP_TOKEN_FIELD})
notifyDataSetChanged()
}
}
@@ -272,10 +281,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB))
}
private fun actionGoAutomatically() {
private fun actionGoAutomatically(switchToPreviousKeyboardIfAllowed: Boolean = true) {
if (PreferencesUtil.isAutoGoActionEnable(this)) {
currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO)
if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
if (switchToPreviousKeyboardIfAllowed
&& PreferencesUtil.isKeyboardPreviousFillInEnable(this)) {
switchToPreviousKeyboard()
}
}
@@ -326,6 +336,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
private const val KEY_ENTRY = 620
private const val KEY_USERNAME = 500
private const val KEY_PASSWORD = 510
private const val KEY_OTP = 515
private const val KEY_URL = 520
private const val KEY_FIELDS = 530

View File

@@ -39,6 +39,8 @@ class EntryInfo : Parcelable {
var icon: IconImage = IconImageStandard()
var username: String = ""
var password: String = ""
var creationTime: DateInstant = DateInstant()
var modificationTime: DateInstant = DateInstant()
var expires: Boolean = false
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
var url: String = ""
@@ -55,6 +57,8 @@ class EntryInfo : Parcelable {
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
username = parcel.readString() ?: username
password = parcel.readString() ?: password
creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime
modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime
expires = parcel.readInt() != 0
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
url = parcel.readString() ?: url
@@ -74,6 +78,8 @@ class EntryInfo : Parcelable {
parcel.writeParcelable(icon, flags)
parcel.writeString(username)
parcel.writeString(password)
parcel.writeParcelable(creationTime, flags)
parcel.writeParcelable(modificationTime, flags)
parcel.writeInt(if (expires) 1 else 0)
parcel.writeParcelable(expiryTime, flags)
parcel.writeString(url)
@@ -91,13 +97,13 @@ class EntryInfo : Parcelable {
return customFields.any { !it.protectedValue.isProtected }
}
fun isAutoGeneratedField(field: Field): Boolean {
return field.name == OTP_TOKEN_FIELD
fun containsCustomField(label: String): Boolean {
return customFields.lastOrNull { it.name == label } != null
}
fun getGeneratedFieldValue(label: String): String {
otpModel?.let {
if (label == OTP_TOKEN_FIELD) {
if (label == OTP_TOKEN_FIELD) {
otpModel?.let {
return OtpElement(it).token
}
}

View File

@@ -0,0 +1,32 @@
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,82 @@
package com.kunzisoft.keepass.model
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.text.format.Formatter
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import java.text.DateFormat
import java.util.*
/**
* Utility data class to get FileDatabaseInfo at a `t` time
*/
data class SnapFileDatabaseInfo(var fileUri: Uri?,
var exists: Boolean,
var lastModification: Long?,
var size: Long?): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(Uri::class.java.classLoader),
parcel.readByte() != 0.toByte(),
parcel.readValue(Long::class.java.classLoader) as? Long,
parcel.readValue(Long::class.java.classLoader) as? Long) {
}
fun toString(context: Context): String {
val lastModificationString = DateFormat.getDateTimeInstance()
.format(Date(lastModification ?: 0))
return "$lastModificationString, " +
Formatter.formatFileSize(context, size ?: 0)
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(fileUri, flags)
parcel.writeByte(if (exists) 1 else 0)
parcel.writeValue(lastModification)
parcel.writeValue(size)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SnapFileDatabaseInfo) return false
if (fileUri != other.fileUri) return false
if (exists != other.exists) return false
if (lastModification != other.lastModification) return false
if (size != other.size) return false
return true
}
override fun hashCode(): Int {
var result = fileUri?.hashCode() ?: 0
result = 31 * result + exists.hashCode()
result = 31 * result + (lastModification?.hashCode() ?: 0)
result = 31 * result + (size?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<SnapFileDatabaseInfo> {
override fun createFromParcel(parcel: Parcel): SnapFileDatabaseInfo {
return SnapFileDatabaseInfo(parcel)
}
override fun newArray(size: Int): Array<SnapFileDatabaseInfo?> {
return arrayOfNulls(size)
}
fun fromFileDatabaseInfo(fileDatabaseInfo: FileDatabaseInfo): SnapFileDatabaseInfo {
return SnapFileDatabaseInfo(
fileDatabaseInfo.fileUri,
fileDatabaseInfo.exists,
fileDatabaseInfo.getLastModification(),
fileDatabaseInfo.getSize())
}
}
}

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.otp
import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.binary.Hex
@@ -137,7 +138,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@Throws(IllegalArgumentException::class)
fun setHexSecret(secret: String) {
if (secret.isNotEmpty())
otpModel.secret = Hex.decodeHex(secret)
otpModel.secret = Hex.decodeHex(secret.toCharArray())
else
throw IllegalArgumentException()
}
@@ -150,16 +151,16 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
@Throws(IllegalArgumentException::class)
fun setBase32Secret(secret: String) {
if (isValidBase32(secret))
otpModel.secret = Base32().decode(replaceBase32Chars(secret).toByteArray())
else
if (isValidBase32(secret)) {
otpModel.secret = Base32().decode(replaceBase32Chars(secret))
} else
throw IllegalArgumentException()
}
@Throws(IllegalArgumentException::class)
fun setBase64Secret(secret: String) {
if (isValidBase64(secret))
otpModel.secret = Base64().decode(secret.toByteArray())
otpModel.secret = Base64().decode(secret)
else
throw IllegalArgumentException()
}
@@ -208,38 +209,24 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
fun isValidBase32(secret: String): Boolean {
val secretChars = replaceBase32Chars(secret)
return secretChars.isNotEmpty() && checkBase32Secret(secretChars)
return secret.isNotEmpty()
&& (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secretChars))
}
fun isValidBase64(secret: String): Boolean {
// TODO replace base 64 chars
return secret.isNotEmpty() && checkBase64Secret(secret)
}
fun removeLineChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
}
fun removeSpaceChars(parameter: String): String {
return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
return secret.isNotEmpty()
&& (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
}
fun replaceBase32Chars(parameter: String): String {
// Add 'A' at end if not Base32 length
var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH))
// Add padding '=' at end if not Base32 length
var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars()
while (parameterNewSize.length % 8 != 0) {
parameterNewSize += 'A'
parameterNewSize += '='
}
return parameterNewSize
}
fun checkBase32Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret))
}
fun checkBase64Secret(secret: String): Boolean {
return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret))
}
}
}

View File

@@ -24,9 +24,9 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars
import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars
import com.kunzisoft.keepass.otp.TokenCalculator.*
import com.kunzisoft.keepass.utils.StringUtil.removeLineChars
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import java.util.*
import java.util.regex.Pattern
@@ -57,13 +57,25 @@ object OtpEntryFields {
private const val DIGITS_KEY = "size"
private const val STEP_KEY = "step"
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp)
// HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#otp)
private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret"
private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex"
private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32"
private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64"
private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter"
// TimeOtp KeePass2 values
private const val TIMEOTP_SECRET_FIELD = "TimeOtp-Secret"
private const val TIMEOTP_SECRET_HEX_FIELD = "TimeOtp-Secret-Hex"
private const val TIMEOTP_SECRET_BASE32_FIELD = "TimeOtp-Secret-Base32"
private const val TIMEOTP_SECRET_BASE64_FIELD = "TimeOtp-Secret-Base64"
private const val TIMEOTP_LENGTH_FIELD = "TimeOtp-Length"
private const val TIMEOTP_PERIOD_FIELD = "TimeOtp-Period"
private const val TIMEOTP_ALGORITHM_FIELD = "TimeOtp-Algorithm"
private const val TIMEOTP_ALGORITHM_SHA1_VALUE = "HMAC-SHA-1"
private const val TIMEOTP_ALGORITHM_SHA256_VALUE = "HMAC-SHA-256"
private const val TIMEOTP_ALGORITHM_SHA512_VALUE = "HMAC-SHA-512"
// Custom fields (maybe from plugin)
private const val TOTP_SEED_FIELD = "TOTP Seed"
private const val TOTP_SETTING_FIELD = "TOTP Settings"
@@ -85,14 +97,17 @@ object OtpEntryFields {
// OTP (HOTP/TOTP) from URL and field from KeePassXC
if (parseOTPUri(getField, otpElement))
return otpElement
// TOTP from KeePass 2.47
if (parseTOTPFromOfficialField(getField, otpElement))
return otpElement
// TOTP from key values (maybe plugin or old KeePassXC)
if (parseTOTPKeyValues(getField, otpElement))
return otpElement
// TOTP from custom field
if (parseTOTPFromField(getField, otpElement))
if (parseTOTPFromPluginField(getField, otpElement))
return otpElement
// HOTP fields from KeePass 2
if (parseHOTPFromField(getField, otpElement))
if (parseHOTPFromOfficialField(getField, otpElement))
return otpElement
return null
}
@@ -126,7 +141,7 @@ object OtpEntryFields {
private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val otpPlainText = getField(OTP_FIELD)
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
val uri = Uri.parse(removeSpaceChars(otpPlainText))
val uri = Uri.parse(otpPlainText.removeSpaceChars())
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
Log.e(TAG, "Invalid or missing scheme in uri")
@@ -159,16 +174,16 @@ object OtpEntryFields {
if (nameParam != null && nameParam.isNotEmpty()) {
val userIdArray = nameParam.split(":", "%3A")
if (userIdArray.size > 1) {
otpElement.issuer = removeLineChars(userIdArray[0])
otpElement.name = removeLineChars(userIdArray[1])
otpElement.issuer = userIdArray[0].removeLineChars()
otpElement.name = userIdArray[1].removeLineChars()
} else {
otpElement.name = removeLineChars(nameParam)
otpElement.name = nameParam.removeLineChars()
}
}
val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM)
if (issuerParam != null && issuerParam.isNotEmpty())
otpElement.issuer = removeLineChars(issuerParam)
otpElement.issuer = issuerParam.removeLineChars()
val secretParam = uri.getQueryParameter(SECRET_URL_PARAM)
if (secretParam != null && secretParam.isNotEmpty()) {
@@ -247,8 +262,9 @@ object OtpEntryFields {
encodeParameter(username)
else
encodeParameter(otpElement.name)
val secret = encodeParameter(otpElement.getBase32Secret())
val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" +
"?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" +
"?$SECRET_URL_PARAM=${secret}" +
"&$counterOrPeriodLabel=$counterOrPeriodValue" +
"&$DIGITS_URL_PARAM=${otpElement.digits}" +
"&$ISSUER_URL_PARAM=$issuer")
@@ -262,7 +278,40 @@ object OtpEntryFields {
}
private fun encodeParameter(parameter: String): String {
return Uri.encode(OtpElement.removeLineChars(parameter))
return Uri.encode(parameter.removeLineChars())
}
private fun parseTOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val secretField = getField(TIMEOTP_SECRET_FIELD)
val secretHexField = getField(TIMEOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(TIMEOTP_SECRET_BASE32_FIELD)
val secretBase64Field = getField(TIMEOTP_SECRET_BASE64_FIELD)
val lengthField = getField(TIMEOTP_LENGTH_FIELD)
val periodField = getField(TIMEOTP_PERIOD_FIELD)
val algorithmField = getField(TIMEOTP_ALGORITHM_FIELD)
try {
when {
secretField != null -> otpElement.setUTF8Secret(secretField)
secretHexField != null -> otpElement.setHexSecret(secretHexField)
secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field)
secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field)
lengthField != null -> otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS
periodField != null -> otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
algorithmField != null -> otpElement.algorithm =
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
else -> HashAlgorithm.SHA1
}
else -> return false
}
} catch (exception: Exception) {
return false
}
otpElement.type = OtpType.TOTP
return true
}
private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
@@ -290,7 +339,7 @@ object OtpEntryFields {
return false
}
private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
private fun parseTOTPFromPluginField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val seedField = getField(TOTP_SEED_FIELD) ?: return false
try {
otpElement.setBase32Secret(seedField)
@@ -305,9 +354,15 @@ object OtpEntryFields {
return false
}
otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD
otpElement.tokenType = matcher.group(2)?.let {
OtpTokenType.getFromString(it)
} ?: OtpTokenType.RFC6238
matcher.group(2)?.let { secondMatcher ->
try {
otpElement.digits = secondMatcher.toInt()
} catch (e: NumberFormatException) {
otpElement.digits = OTP_DEFAULT_DIGITS
otpElement.tokenType = OtpTokenType.getFromString(secondMatcher)
}
}
}
} catch (exception: Exception) {
return false
@@ -316,7 +371,7 @@ object OtpEntryFields {
return true
}
private fun parseHOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
private fun parseHOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean {
val secretField = getField(HMACOTP_SECRET_FIELD)
val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD)
val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD)
@@ -382,25 +437,43 @@ object OtpEntryFields {
val totpSeedField = Field(TOTP_SEED_FIELD)
val totpSettingField = Field(TOTP_SETTING_FIELD)
val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD)
val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD)
val hmacOtpSecretHexField = Field(HMACOTP_SECRET_HEX_FIELD)
val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD)
val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD)
val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD)
val timeOtpSecretField = Field(TIMEOTP_SECRET_FIELD)
val timeOtpSecretHexField = Field(TIMEOTP_SECRET_HEX_FIELD)
val timeOtpSecretBase32Field = Field(TIMEOTP_SECRET_BASE32_FIELD)
val timeOtpSecretBase64Field = Field(TIMEOTP_SECRET_BASE64_FIELD)
val timeOtpLengthField = Field(TIMEOTP_LENGTH_FIELD)
val timeOtpPeriodField = Field(TIMEOTP_PERIOD_FIELD)
val timeOtpAlgorithmField = Field(TIMEOTP_ALGORITHM_FIELD)
newCustomFields.remove(otpField)
newCustomFields.remove(totpSeedField)
newCustomFields.remove(totpSettingField)
newCustomFields.remove(hmacOtpSecretField)
newCustomFields.remove(hmacOtpSecretHewField)
newCustomFields.remove(hmacOtpSecretHexField)
newCustomFields.remove(hmacOtpSecretBase32Field)
newCustomFields.remove(hmacOtpSecretBase64Field)
newCustomFields.remove(hmacOtpSecretCounterField)
newCustomFields.remove(timeOtpSecretField)
newCustomFields.remove(timeOtpSecretHexField)
newCustomFields.remove(timeOtpSecretBase32Field)
newCustomFields.remove(timeOtpSecretBase64Field)
newCustomFields.remove(timeOtpLengthField)
newCustomFields.remove(timeOtpPeriodField)
newCustomFields.remove(timeOtpAlgorithmField)
// Empty auto generated OTP Token field
if (fieldsToParse.contains(otpField)
|| fieldsToParse.contains(totpSeedField)
|| fieldsToParse.contains(hmacOtpSecretField)
|| fieldsToParse.contains(hmacOtpSecretHewField)
|| fieldsToParse.contains(hmacOtpSecretHexField)
|| fieldsToParse.contains(hmacOtpSecretBase32Field)
|| fieldsToParse.contains(hmacOtpSecretBase64Field)
|| fieldsToParse.contains(timeOtpSecretField)
|| fieldsToParse.contains(timeOtpSecretHexField)
|| fieldsToParse.contains(timeOtpSecretBase32Field)
|| fieldsToParse.contains(timeOtpSecretBase64Field)
)
newCustomFields.add(Field(OTP_TOKEN_FIELD))
return newCustomFields

View File

@@ -0,0 +1,143 @@
package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.IBinder
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
import kotlinx.coroutines.*
class AdvancedUnlockNotificationService : NotificationService() {
private lateinit var mTempCipherDao: ArrayList<CipherDatabaseEntity>
private var mActionTaskBinder = AdvancedUnlockBinder()
private var notificationTimeoutMilliSecs: Long = 0
private var mTimerJob: Job? = null
inner class AdvancedUnlockBinder: Binder() {
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
}
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity) {
val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull { it.databaseUri == cipherDatabaseEntity.databaseUri }
cipherDatabaseRetrieve?.replaceContent(cipherDatabaseEntity)
?: mTempCipherDao.add(cipherDatabaseEntity)
}
fun deleteByDatabaseUri(databaseUri: Uri) {
mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString() }?.let {
mTempCipherDao.remove(it)
}
}
fun deleteAll() {
mTempCipherDao.clear()
}
}
override val notificationId: Int = 593
override fun retrieveChannelId(): String {
return CHANNEL_ADVANCED_UNLOCK_ID
}
override fun retrieveChannelName(): String {
return getString(R.string.advanced_unlock)
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val deleteIntent = Intent(this, AdvancedUnlockNotificationService::class.java).apply {
action = ACTION_REMOVE_KEYS
}
val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
val notificationBuilder = buildNewNotification().apply {
setSmallIcon(if (biometricUnlockEnabled) {
R.drawable.notification_ic_fingerprint_unlock_24dp
} else {
R.drawable.notification_ic_device_unlock_24dp
})
intent?.let {
setContentTitle(getString(R.string.advanced_unlock))
}
setContentText(getString(R.string.advanced_unlock_tap_delete))
setContentIntent(pendingDeleteIntent)
// Unfortunately swipe is disabled in lollipop+
setDeleteIntent(pendingDeleteIntent)
}
when (intent?.action) {
ACTION_TIMEOUT -> {
notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
val maxPos = 100
val posDurationMills = notificationTimeoutMilliSecs / maxPos
for (pos in maxPos downTo 0) {
notificationBuilder.setProgress(maxPos, pos, false)
startForeground(notificationId, notificationBuilder.build())
delay(posDurationMills)
if (pos <= 0) {
stopSelf()
}
}
notificationManager?.cancel(notificationId)
mTimerJob = null
cancel()
}
} else {
startForeground(notificationId, notificationBuilder.build())
}
}
ACTION_REMOVE_KEYS -> {
stopSelf()
}
else -> {}
}
return START_STICKY
}
override fun onCreate() {
super.onCreate()
mTempCipherDao = ArrayList()
}
override fun onDestroy() {
mTempCipherDao.clear()
mTimerJob?.cancel()
super.onDestroy()
}
companion object {
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
private const val ACTION_TIMEOUT = "ACTION_TIMEOUT"
private const val ACTION_REMOVE_KEYS = "ACTION_REMOVE_KEYS"
fun startServiceForTimeout(context: Context) {
if (PreferencesUtil.isTempAdvancedUnlockEnable(context)) {
context.startService(Intent(context, AdvancedUnlockNotificationService::class.java).apply {
action = ACTION_TIMEOUT
})
}
}
fun stopService(context: Context) {
context.stopService(Intent(context, AdvancedUnlockNotificationService::class.java))
}
}
}

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.ContentResolver

View File

@@ -17,12 +17,13 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import java.util.*
/**
@@ -54,6 +55,7 @@ class ClipboardEntryNotificationField : Parcelable {
NotificationFieldId.UNKNOWN -> ""
NotificationFieldId.USERNAME -> entryInfo?.username ?: ""
NotificationFieldId.PASSWORD -> entryInfo?.password ?: ""
NotificationFieldId.OTP -> entryInfo?.getGeneratedFieldValue(OTP_TOKEN_FIELD) ?: ""
NotificationFieldId.FIELD_A,
NotificationFieldId.FIELD_B,
NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: ""
@@ -81,7 +83,7 @@ class ClipboardEntryNotificationField : Parcelable {
}
enum class NotificationFieldId {
UNKNOWN, USERNAME, PASSWORD, FIELD_A, FIELD_B, FIELD_C;
UNKNOWN, USERNAME, PASSWORD, OTP, FIELD_A, FIELD_B, FIELD_C;
companion object {
val anonymousFieldId: Array<NotificationFieldId>

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context
@@ -25,6 +25,7 @@ import android.content.Intent
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
@@ -250,6 +251,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsUsernameToCopy = entry.username.isNotEmpty()
val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
&& (entry.containsCustomFieldsNotProtected()
||
@@ -262,7 +264,10 @@ class ClipboardEntryNotificationService : LockNotificationService() {
// If notifications enabled in settings
// Don't if application timeout
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) {
if (containsUsernameToCopy
|| containsPasswordToCopy
|| containsOTPToCopy
|| containsExtraFieldToCopy) {
// username already copied, waiting for user's action before copy password.
intent.action = ACTION_NEW_NOTIFICATION
@@ -282,14 +287,22 @@ class ClipboardEntryNotificationService : LockNotificationService() {
ClipboardEntryNotificationField.NotificationFieldId.PASSWORD,
context.getString(R.string.entry_password)))
}
// Add OTP
if (containsOTPToCopy) {
notificationFields.add(
ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.OTP,
OTP_TOKEN_FIELD))
}
// Add extra fields
if (containsExtraFieldToCopy) {
try {
var anonymousFieldNumber = 0
entry.customFields.forEach { field ->
//If value is not protected or allowed
if (!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) {
if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(context))
&& field.name != OTP_TOKEN_FIELD) {
notificationFields.add(
ClipboardEntryNotificationField(
ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber],

View File

@@ -17,14 +17,13 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.*
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
@@ -40,6 +39,8 @@ 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.model.MainCredential
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -47,6 +48,7 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.*
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
@@ -65,6 +67,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mAllowFinishAction = AtomicBoolean()
private var mActionRunning = false
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
private var mIconId: Int = R.drawable.notification_ic_database_load
private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null
@@ -93,6 +97,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mAllowFinishAction.set(false)
}
}
fun addDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) {
mDatabaseInfoListeners.add(databaseInfoListener)
}
fun removeDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) {
mDatabaseInfoListeners.remove(databaseInfoListener)
}
}
interface ActionTaskListener {
@@ -101,6 +113,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
fun onStopAction(actionTask: String, result: ActionRunnable.Result)
}
interface DatabaseInfoListener {
fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
newDatabaseInfo: SnapFileDatabaseInfo)
}
/**
* Force to call [ActionTaskListener.onStartAction] if the action is still running
*/
@@ -112,6 +129,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
fun checkDatabaseInfo() {
try {
mDatabase.fileUri?.let {
val previousDatabaseInfo = mSnapFileDatabaseInfo
val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
val oldDatabaseModification = previousDatabaseInfo?.lastModification
val newDatabaseModification = lastFileDatabaseInfo.lastModification
val conditionExists = previousDatabaseInfo != null
&& previousDatabaseInfo.exists != lastFileDatabaseInfo.exists
// To prevent dialog opening too often
val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null
&& oldDatabaseModification < newDatabaseModification
&& mLastLocalSaveTime + 5000 < newDatabaseModification)
if (conditionExists || conditionLastModification) {
// Show the dialog only if it's real new info and not a delay after a save
Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info
if (previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
}
}
mSnapFileDatabaseInfo = lastFileDatabaseInfo
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check database info", e)
}
}
fun saveDatabaseInfo() {
try {
mDatabase.fileUri?.let {
mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to check database info", e)
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return mActionTaskBinder
@@ -138,6 +202,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent)
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask()
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent)
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent)
ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent)
@@ -192,6 +257,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
actionTaskListener.onStopAction(intentAction!!, result)
}
} finally {
// Save the database info before performing action
if (intentAction == ACTION_DATABASE_LOAD_TASK) {
saveDatabaseInfo()
}
// Save the database info after performing save action
if (intentAction == ACTION_DATABASE_SAVE
|| intent?.getBooleanExtra(SAVE_DATABASE_KEY, false) == true) {
mDatabase.fileUri?.let {
val newSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
FileDatabaseInfo(applicationContext, it))
mLastLocalSaveTime = System.currentTimeMillis()
mSnapFileDatabaseInfo = newSnapFileDatabaseInfo
}
}
removeIntentData(intent)
TimeoutHelper.releaseTemporarilyDisableTimeout()
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
@@ -214,7 +293,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
return when (intentAction) {
ACTION_DATABASE_LOAD_TASK, null -> {
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_RELOAD_TASK,
null -> {
START_STICKY
}
else -> {
@@ -248,7 +329,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
else -> {
when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
ACTION_DATABASE_LOAD_TASK -> R.string.loading_database
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_SAVE -> R.string.saving_database
else -> {
R.string.command_execution
@@ -258,13 +340,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
mMessageId = when (intentAction) {
ACTION_DATABASE_LOAD_TASK -> null
ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_RELOAD_TASK -> null
else -> null
}
mWarningId =
if (!saveAction
|| intentAction == ACTION_DATABASE_LOAD_TASK)
|| intentAction == ACTION_DATABASE_LOAD_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
null
else
R.string.do_not_kill_app
@@ -316,10 +400,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
intent?.removeExtra(DATABASE_TASK_WARNING_KEY)
intent?.removeExtra(DATABASE_URI_KEY)
intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY)
intent?.removeExtra(MASTER_PASSWORD_KEY)
intent?.removeExtra(KEY_FILE_CHECKED_KEY)
intent?.removeExtra(KEY_FILE_URI_KEY)
intent?.removeExtra(MAIN_CREDENTIAL_KEY)
intent?.removeExtra(READ_ONLY_KEY)
intent?.removeExtra(CIPHER_ENTITY_KEY)
intent?.removeExtra(FIX_DUPLICATE_UUID_KEY)
@@ -342,9 +423,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
* Execute action with a coroutine
*/
private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater,
onPreExecute: () -> Unit,
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
onPreExecute: () -> Unit,
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
mAllowFinishAction.set(false)
TimeoutHelper.temporarilyDisableTimeout()
@@ -391,13 +472,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
if (databaseUri == null)
return null
@@ -407,14 +485,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseUri,
getString(R.string.database_default_name),
getString(R.string.database),
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
keyFileUri
mainCredential
) { result ->
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
}
}
} else {
@@ -425,15 +500,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
&& intent.hasExtra(READ_ONLY_KEY)
&& intent.hasExtra(CIPHER_ENTITY_KEY)
&& intent.hasExtra(FIX_DUPLICATE_UUID_KEY)
) {
val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY)
val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY)
val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY)
val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true)
val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY)
@@ -444,8 +517,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
this,
mDatabase,
databaseUri,
masterPassword,
keyFileUri,
mainCredential,
readOnly,
cipherEntity,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
@@ -454,8 +526,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
// Add each info to reload database after thrown duplicate UUID exception
result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri)
putString(MASTER_PASSWORD_KEY, masterPassword)
putParcelable(KEY_FILE_URI_KEY, keyFileUri)
putParcelable(MAIN_CREDENTIAL_KEY, mainCredential)
putBoolean(READ_ONLY_KEY, readOnly)
putParcelable(CIPHER_ENTITY_KEY, cipherEntity)
}
@@ -465,21 +536,26 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
}
private fun buildDatabaseReloadActionTask(): ActionRunnable {
return ReloadDatabaseRunnable(
this,
mDatabase,
this
) { result ->
// No need to add each info to reload database
result.data = Bundle()
}
}
private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(DATABASE_URI_KEY)
&& intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY)
&& intent.hasExtra(MASTER_PASSWORD_KEY)
&& intent.hasExtra(KEY_FILE_CHECKED_KEY)
&& intent.hasExtra(KEY_FILE_URI_KEY)
&& intent.hasExtra(MAIN_CREDENTIAL_KEY)
) {
val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null
AssignPasswordInDatabaseRunnable(this,
mDatabase,
databaseUri,
intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false),
intent.getStringExtra(MASTER_PASSWORD_KEY),
intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false),
intent.getParcelableExtra(KEY_FILE_URI_KEY)
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
)
} else {
null
@@ -770,6 +846,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK"
const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK"
const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK"
const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK"
const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK"
const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK"
@@ -801,10 +878,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY"
const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY"
const val READ_ONLY_KEY = "READ_ONLY_KEY"
const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY"
const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY"
@@ -822,6 +896,9 @@ 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

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.app.PendingIntent
import android.content.Context

View File

@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.content.Intent
import com.kunzisoft.keepass.utils.LockReceiver

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.notifications
package com.kunzisoft.keepass.services
import android.app.NotificationChannel
import android.app.NotificationManager

View File

@@ -19,10 +19,12 @@
*/
package com.kunzisoft.keepass.settings
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
@@ -32,6 +34,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences_autofill, rootKey)
val autofillInlineSuggestionsPreference: SwitchPreference? = findPreference(getString(R.string.autofill_inline_suggestions_key))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
autofillInlineSuggestionsPreference?.isVisible = false
}
}
override fun onDisplayPreferenceDialog(preference: Preference?) {

View File

@@ -103,6 +103,6 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
}
interface Callback {
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen)
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean = false)
}
}

View File

@@ -41,9 +41,10 @@ import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.icons.IconPackChooser
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
import com.kunzisoft.keepass.utils.UriUtil
@@ -214,9 +215,10 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
val deviceCredentialUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.device_credential_unlock_enable_key))
val autoOpenPromptPreference: SwitchPreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key))
val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BiometricUnlockDatabaseHelper.biometricUnlockSupported(activity)
AdvancedUnlockManager.biometricUnlockSupported(activity)
} else false
biometricUnlockEnablePreference?.apply {
// False if under Marshmallow
@@ -237,6 +239,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
deleteKeysMessage(activity) {
biometricUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
}
} else {
if (deviceCredentialChecked) {
@@ -247,6 +250,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
} else {
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
}
}
true
@@ -254,15 +258,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
}
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
BiometricUnlockDatabaseHelper.deviceCredentialUnlockSupported(activity)
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
} else false
deviceCredentialUnlockEnablePreference?.apply {
// Biometric unlock already checked
if (biometricUnlockEnablePreference?.isChecked == true)
isChecked = false
if (!deviceCredentialUnlockSupported) {
isChecked = false
setOnPreferenceClickListener { preference ->
(preference as SwitchPreference).isChecked = false
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.R)
UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.M)
.show(parentFragmentManager, "unavailableFeatureDialog")
false
}
@@ -275,6 +282,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
deleteKeysMessage(activity) {
deviceCredentialUnlockEnablePreference.isChecked = false
autoOpenPromptPreference?.isEnabled = biometricChecked
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
}
} else {
if (biometricChecked) {
@@ -285,6 +293,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
} else {
autoOpenPromptPreference?.isEnabled = true
tempAdvancedUnlockPreference?.isEnabled = true
}
}
true
@@ -294,6 +303,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|| deviceCredentialUnlockEnablePreference?.isChecked == true
tempAdvancedUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|| deviceCredentialUnlockEnablePreference?.isChecked == true
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
deleteKeysMessage(activity) {
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
}
true
}
val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key))
if (biometricUnlockSupported || deviceCredentialUnlockSupported) {
@@ -321,9 +340,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
validate?.invoke()
deleteKeysAlertDialog?.setOnDismissListener(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric(
AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
activity,
object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback {
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(context,
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
@@ -334,11 +353,12 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
showException(e)
}
override fun onBiometricException(e: Exception) {
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
AdvancedUnlockNotificationService.stopService(activity.applicationContext)
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
.setNegativeButton(resources.getString(android.R.string.cancel)
@@ -366,7 +386,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
if (styleEnabled) {
Stylish.assignStyle(styleIdString)
activity.recreate()
// Relaunch the current activity to redraw theme
(activity as? SettingsActivity?)?.apply {
keepCurrentScreen()
startActivity(intent)
finish()
activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
}
}
styleEnabled
}

View File

@@ -35,7 +35,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.preference.*
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -58,7 +58,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null
private var mKeyDerivationPref: DialogListExplanationPreference? = null
private var mRoundPref: InputKdfNumberPreference? = null
private var mMemoryPref: InputKdfNumberPreference? = null
private var mMemoryPref: InputKdfSizePreference? = null
private var mParallelismPref: InputKdfNumberPreference? = null
override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) {
@@ -231,7 +231,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
}
// Memory Usage
mMemoryPref = findPreference<InputKdfNumberPreference>(getString(R.string.memory_usage_key))?.apply {
mMemoryPref = findPreference<InputKdfSizePreference>(getString(R.string.memory_usage_key))?.apply {
summary = mDatabase.memoryUsage.toString()
}
@@ -552,6 +552,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
true
}
R.id.menu_reload_database -> {
settingActivity?.apply {
keepCurrentScreen()
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
}
return true
}
else -> {
// Check the time lock before launching settings

View File

@@ -35,9 +35,13 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
}
fun getScreen(): Screen {
return Screen.values()[requireArguments().getInt(TAG_KEY)]
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
onCreateScreenPreference(
Screen.values()[requireArguments().getInt(TAG_KEY)],
getScreen(),
savedInstanceState,
rootKey)
}

View File

@@ -19,12 +19,14 @@
*/
package com.kunzisoft.keepass.settings
import android.app.backup.BackupManager
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.timeout.TimeoutHelper
import java.util.*
@@ -43,6 +45,7 @@ object PreferencesUtil {
}
apply()
}
BackupManager(context).dataChanged()
}
fun getDefaultDatabasePath(context: Context): String? {
@@ -201,6 +204,13 @@ object PreferencesUtil {
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun getAdvancedUnlockTimeout(context: Context): Long {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.temp_advanced_unlock_timeout_key),
context.getString(R.string.temp_advanced_unlock_timeout_default))?.toLong()
?: TimeoutHelper.DEFAULT_TIMEOUT
}
fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key),
@@ -231,8 +241,29 @@ object PreferencesUtil {
fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(context)
} else {
false
}
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
&& biometricSupported
}
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Priority to biometric unlock
val biometricAlreadySupported = isBiometricUnlockEnable(context)
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
&& !biometricAlreadySupported
}
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.temp_advanced_unlock_enable_key),
context.resources.getBoolean(R.bool.temp_advanced_unlock_enable_default))
}
fun isAdvancedUnlockPromptAutoOpenEnable(context: Context): Boolean {
@@ -241,12 +272,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
}
fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
}
fun getListSort(context: Context): SortNodeEnum {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.getString(context.getString(R.string.sort_node_key),
@@ -411,13 +436,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.autofill_close_database_default))
}
fun isAutofillAutoSearchEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key),
context.resources.getBoolean(R.bool.autofill_auto_search_default))
}
fun isAutofillInlineSuggestionsEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key),
context.resources.getBoolean(R.bool.autofill_inline_suggestions_default))
}
fun isAutofillSaveSearchInfoEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key),

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
* Copyright 2020 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
import android.app.Activity
import android.app.backup.BackupManager
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -35,9 +34,12 @@ import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
open class SettingsActivity
: LockingActivity(),
@@ -81,7 +83,7 @@ open class SettingsActivity
}
// Focus view to reinitialize timeout
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
@@ -94,12 +96,35 @@ open class SettingsActivity
backupManager = BackupManager(this)
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
// Call result in fragment
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
?.onProgressDialogThreadResult(actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
if (result.isSuccess) {
startActivity(intent)
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
} else {
this.showActionErrorIfNeeded(result)
finish()
}
}
else -> {
// Call result in fragment
(supportFragmentManager
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
?.onProgressDialogThreadResult(actionTask, result)
}
}
coordinatorLayout?.showActionErrorIfNeeded(result)
}
coordinatorLayout?.showActionError(result)
// To reload the current screen
if (intent.extras?.containsKey(FRAGMENT_ARG) == true) {
intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName ->
onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true)
}
// Eat state
intent.removeExtra(FRAGMENT_ARG)
}
}
@@ -117,52 +142,33 @@ open class SettingsActivity
}
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
mainCredential: MainCredential) {
databaseUri?.let {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
mainCredential
)
}
}
override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
Database.getInstance().let { database ->
database.fileUri?.let { databaseUri ->
// Show the progress dialog now or after dialog confirmation
if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) {
if (database.validatePasswordEncoding(mainCredential)) {
mProgressDatabaseTaskProvider?.startDatabaseAssignPassword(
databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
mainCredential
)
} else {
PasswordEncodingDialogFragment.getInstance(databaseUri,
masterPasswordChecked,
masterPassword,
keyFileChecked,
keyFile
).show(supportFragmentManager, "passwordEncodingTag")
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
.show(supportFragmentManager, "passwordEncodingTag")
}
}
}
}
override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean,
masterPassword: String?,
keyFileChecked: Boolean,
keyFile: Uri?) {}
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) {
if (PreferencesUtil.showLockDatabaseButton(this)) {
@@ -192,25 +198,41 @@ open class SettingsActivity
hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION)
}
private fun replaceFragment(key: NestedSettingsFragment.Screen) {
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
private fun replaceFragment(key: NestedSettingsFragment.Screen, reload: Boolean) {
supportFragmentManager.beginTransaction().apply {
if (reload) {
setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out,
R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED)
.addToBackStack(TAG_NESTED)
.commit()
} else {
setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
R.anim.slide_in_left, R.anim.slide_out_right)
}
replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED)
addToBackStack(TAG_NESTED)
commit()
}
toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key)
hideOrShowLockButton(key)
}
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {
/**
* To keep the current screen when activity is reloaded
*/
fun keepCurrentScreen() {
(supportFragmentManager.findFragmentByTag(TAG_NESTED) as? NestedSettingsFragment?)
?.getScreen()?.let { fragmentKey ->
intent.putExtra(FRAGMENT_ARG, fragmentKey.name)
}
}
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) {
if (mTimeoutEnable)
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
replaceFragment(key)
replaceFragment(key, reload)
}
else
replaceFragment(key)
replaceFragment(key, reload)
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -225,6 +247,7 @@ open class SettingsActivity
private const val SHOW_LOCK = "SHOW_LOCK"
private const val TITLE_KEY = "TITLE_KEY"
private const val TAG_NESTED = "TAG_NESTED"
private const val FRAGMENT_ARG = "FRAGMENT_ARG"
fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) {
val intent = Intent(activity, SettingsActivity::class.java)

View File

@@ -25,7 +25,7 @@ import androidx.preference.DialogPreference
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2019 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.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class InputKdfSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
if (summary == UNKNOWN_VALUE_STRING) {
super.setSummary("")
} else {
var summaryString = summary
try {
val memorySize = summary.toString().toLong()
summaryString = if (memorySize > 0) {
// To convert bytes to mebibytes
DataByte(memorySize, DataByte.ByteFormat.BYTE)
.toBetterByteFormat().toString(context)
} else {
memorySize.toString()
}
} catch (e: Exception) {
} finally {
super.setSummary(summaryString)
}
}
}
}

View File

@@ -36,7 +36,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context,
override fun setSummary(summary: CharSequence) {
if (summary == INFINITE_VALUE_STRING) {
super.setSummary("")
super.setSummary("")
} else {
super.setSummary(summary)
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2019 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.settings.preference
import android.content.Context
import android.util.AttributeSet
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
open class InputSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle,
defStyleRes: Int = defStyleAttr)
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {
override fun setSummary(summary: CharSequence) {
var summaryString = summary
try {
val memorySize = summary.toString().toLong()
summaryString = if (memorySize >= 0) {
// To convert bytes to mebibytes
DataByte(memorySize, DataByte.ByteFormat.BYTE)
.toBetterByteFormat().toString(context)
} else {
memorySize.toString()
}
} catch (e: Exception) {
} finally {
super.setSummary(summaryString)
}
}
}

View File

@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R
abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
private var inputTextView: EditText? = null
private var textUnitView: TextView? = null
private var textExplanationView: TextView? = null
private var switchElementView: CompoundButton? = null
@@ -47,6 +48,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
}
}
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
}
fun setInputTextError(error: CharSequence) {
this.inputTextView?.error = error
}
@@ -55,6 +64,24 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
this.mOnInputTextEditorActionListener = onEditorActionListener
}
var unitText: String?
get() = textUnitView?.text?.toString() ?: ""
set(unitText) {
textUnitView?.apply {
if (unitText != null && unitText.isNotEmpty()) {
text = unitText
visibility = View.VISIBLE
} else {
text = ""
visibility = View.GONE
}
}
}
fun setUnitText(@StringRes unitTextId: Int) {
unitText = getString(unitTextId)
}
var explanationText: String?
get() = textExplanationView?.text?.toString() ?: ""
set(explanationText) {
@@ -69,6 +96,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
}
}
fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId)
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
@@ -93,6 +124,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
}
}
}
textUnitView = view.findViewById(R.id.input_text_unit)
textUnitView?.visibility = View.GONE
textExplanationView = view.findViewById(R.id.explanation_text)
textExplanationView?.visibility = View.GONE
switchElementView = view.findViewById(R.id.switch_element)
@@ -113,18 +146,6 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom
return false
}
fun setInoutText(@StringRes inputTextId: Int) {
inputText = getString(inputTextId)
}
fun showInputText(show: Boolean) {
inputTextView?.visibility = if (show) View.VISIBLE else View.GONE
}
fun setExplanationText(@StringRes explanationTextId: Int) {
explanationText = getString(explanationTextId)
}
fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) {
switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE
switchElementView?.isChecked = defaultChecked

View File

@@ -22,50 +22,76 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var dataByte = DataByte(2L, DataByte.ByteFormat.MEBIBYTE)
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
setExplanationText(R.string.max_history_size_summary)
database?.historyMaxSize?.let { maxItemsDatabase ->
inputText = maxItemsDatabase.toString()
dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
if (dataByte.number >= 0) {
setUnitText(dataByte.format.stringId)
} else {
unitText = null
}
setSwitchAction({ isChecked ->
inputText = if (!isChecked) {
INFINITE_MAX_HISTORY_SIZE.toString()
} else
DEFAULT_MAX_HISTORY_SIZE.toString()
if (!isChecked) {
dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE
inputText = INFINITE_MAX_HISTORY_SIZE.toString()
unitText = null
} else {
dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
showInputText(isChecked)
}, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE)
}
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
database?.let { database ->
var maxHistorySize: Long = try {
val maxHistorySize: Long = try {
inputText.toLong()
} catch (e: NumberFormatException) {
DEFAULT_MAX_HISTORY_SIZE
DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE.toBytes()
}
if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) {
maxHistorySize = INFINITE_MAX_HISTORY_SIZE
val numberOfBytes = if (maxHistorySize >= 0) {
val dataByteConversion = DataByte(maxHistorySize, dataByte.format)
var bytes = dataByteConversion.toBytes()
if (bytes > Long.MAX_VALUE) {
bytes = Long.MAX_VALUE
}
bytes
} else {
INFINITE_MAX_HISTORY_SIZE
}
val oldMaxHistorySize = database.historyMaxSize
database.historyMaxSize = maxHistorySize
database.historyMaxSize = numberOfBytes
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, numberOfBytes, mDatabaseAutoSaveEnable)
}
}
}
companion object {
const val DEFAULT_MAX_HISTORY_SIZE = 134217728L
const val INFINITE_MAX_HISTORY_SIZE = -1L
private val INFINITE_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(INFINITE_MAX_HISTORY_SIZE, DataByte.ByteFormat.MEBIBYTE)
private val DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(6L, DataByte.ByteFormat.MEBIBYTE)
fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat {
val fragment = MaxHistorySizePreferenceDialogFragmentCompat()
val bundle = Bundle(1)

View File

@@ -22,33 +22,46 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.DataByte
class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() {
private var dataByte = DataByte(MIN_MEMORY_USAGE, DataByte.ByteFormat.BYTE)
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
setExplanationText(R.string.memory_usage_explanation)
inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString()
val memoryBytes = database?.memoryUsage ?: MIN_MEMORY_USAGE
dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE)
.toBetterByteFormat()
inputText = dataByte.number.toString()
setUnitText(dataByte.format.stringId)
}
override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult) {
database?.let { database ->
var memoryUsage: Long = try {
var newMemoryUsage: Long = try {
inputText.toLong()
} catch (e: NumberFormatException) {
MIN_MEMORY_USAGE
}
if (memoryUsage < MIN_MEMORY_USAGE) {
memoryUsage = MIN_MEMORY_USAGE
if (newMemoryUsage < MIN_MEMORY_USAGE) {
newMemoryUsage = MIN_MEMORY_USAGE
}
// To transform in bytes
dataByte.number = newMemoryUsage
var numberOfBytes = dataByte.toBytes()
if (numberOfBytes > Long.MAX_VALUE) {
numberOfBytes = Long.MAX_VALUE
}
// TODO Max Memory
val oldMemoryUsage = database.memoryUsage
database.memoryUsage = memoryUsage
database.memoryUsage = numberOfBytes
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable)
mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, numberOfBytes, mDatabaseAutoSaveEnable)
}
}
}

View File

@@ -31,10 +31,10 @@ import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE
class AttachmentFileBinderManager(private val activity: FragmentActivity) {

View File

@@ -32,8 +32,8 @@ import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
@@ -138,5 +138,5 @@ fun Context.closeDatabase() {
cancelAll()
}
// Clear data
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
}

View File

@@ -0,0 +1,84 @@
/*
* 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.utils
import android.content.Context
import androidx.annotation.StringRes
import com.kunzisoft.keepass.R
class DataByte(var number: Long, var format: ByteFormat) {
fun toBetterByteFormat(): DataByte {
return when (this.format) {
ByteFormat.BYTE -> {
when {
//this.number % GIBIBYTES == 0L -> {
// DataByte((this.number / GIBIBYTES), ByteFormat.GIBIBYTE)
//}
this.number % MEBIBYTES == 0L -> {
DataByte((this.number / MEBIBYTES), ByteFormat.MEBIBYTE)
}
this.number % KIBIBYTES == 0L -> {
DataByte((this.number / KIBIBYTES), ByteFormat.KIBIBYTE)
}
else -> {
DataByte(this.number, ByteFormat.BYTE)
}
}
}
else -> {
DataByte(toBytes(), ByteFormat.BYTE).toBetterByteFormat()
}
}
}
/**
* Number of bytes in current DataByte
*/
fun toBytes(): Long {
return when (this.format) {
ByteFormat.BYTE -> this.number
ByteFormat.KIBIBYTE -> this.number * KIBIBYTES
ByteFormat.MEBIBYTE -> this.number * MEBIBYTES
//ByteFormat.GIBIBYTE -> this.number * GIBIBYTES
}
}
override fun toString(): String {
return "$number ${format.name}"
}
fun toString(context: Context): String {
return "$number ${context.getString(format.stringId)}"
}
enum class ByteFormat(@StringRes var stringId: Int) {
BYTE(R.string.unit_byte),
KIBIBYTE(R.string.unit_kibibyte),
MEBIBYTE(R.string.unit_mebibyte)
//GIBIBYTE(R.string.unit_gibibyte)
}
companion object {
const val KIBIBYTES = 1024L
const val MEBIBYTES = 1048576L
const val GIBIBYTES = 1073741824L
}
}

View File

@@ -53,23 +53,19 @@ object MenuUtil {
fun onDefaultMenuOptionsItemSelected(activity: Activity,
item: MenuItem,
readOnly: Boolean = READ_ONLY_DEFAULT,
timeoutEnable: Boolean = false): Boolean {
timeoutEnable: Boolean = false) {
when (item.itemId) {
R.id.menu_contribute -> {
onContributionItemSelected(activity)
return true
}
R.id.menu_app_settings -> {
// To avoid flickering when launch settings in a LockingActivity
SettingsActivity.launch(activity, readOnly, timeoutEnable)
return true
}
R.id.menu_about -> {
val intent = Intent(activity, AboutActivity::class.java)
activity.startActivity(intent)
return true
}
else -> return true
}
}
}

View File

@@ -0,0 +1,14 @@
package com.kunzisoft.keepass.utils
object StringUtil {
fun String.removeLineChars(): String {
return this.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "")
}
fun String.removeSpaceChars(): String {
return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "")
}
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
}

View File

@@ -48,8 +48,8 @@ import java.util.*
class EntryContentsView @JvmOverloads constructor(context: Context,
var attrs: AttributeSet? = null,
var defStyle: Int = 0)
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private var fontInVisibility: Boolean = false
@@ -67,7 +67,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val creationDateView: TextView
private val modificationDateView: TextView
private val lastAccessDateView: TextView
private val expiresImageView: ImageView
private val expiresDateView: TextView
@@ -117,7 +116,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
creationDateView = findViewById(R.id.entry_created)
modificationDateView = findViewById(R.id.entry_modified)
lastAccessDateView = findViewById(R.id.entry_accessed)
expiresImageView = findViewById(R.id.entry_expires_image)
expiresDateView = findViewById(R.id.entry_expires_date)
@@ -258,20 +256,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
modificationDateView.text = date.getDateTimeString(resources)
}
fun assignLastAccessDate(date: DateInstant) {
lastAccessDateView.text = date.getDateTimeString(resources)
}
fun setExpires(isExpires: Boolean) {
fun setExpires(isExpires: Boolean, expiryTime: DateInstant) {
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
}
fun assignExpiresDate(date: DateInstant) {
assignExpiresDate(date.getDateTimeString(resources))
}
fun assignExpiresDate(constString: String) {
expiresDateView.text = constString
expiresDateView.text = if (isExpires) {
expiryTime.getDateTimeString(resources)
} else {
resources.getString(R.string.never)
}
}
fun assignUUID(uuid: UUID) {
@@ -279,7 +270,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
// Hidden style for custom fields
@@ -306,7 +296,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
allowCopy: Boolean,
onCopyButtonClickListener: OnClickListener?) {
val entryCustomField: EntryField? = EntryField(context, attrs, defStyle)
val entryCustomField: EntryField? = EntryField(context)
entryCustomField?.apply {
setLabel(title)
setValue(value.toString(), value.isProtected)

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.documentfile.provider.DocumentFile
@@ -20,7 +19,6 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
private val keyFileNameInputLayout: TextInputLayout
private val keyFileNameView: TextView
private val keyFileOpenView: ImageView
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
@@ -28,7 +26,6 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
keyFileNameInputLayout = findViewById(R.id.input_entry_keyfile)
keyFileNameView = findViewById(R.id.keyfile_name)
keyFileOpenView = findViewById(R.id.keyfile_open_button)
}
override fun setOnClickListener(l: OnClickListener?) {

View File

@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.R
class ToolbarAction @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = androidx.appcompat.R.attr.toolbarStyle)
defStyle: Int = R.attr.actionToolbarAppearance)
: Toolbar(context, attrs, defStyle) {
private var mActionModeCallback: ActionMode.Callback? = null
@@ -39,7 +39,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
private var isOpen = false
init {
visibility = View.GONE
setNavigationIcon(R.drawable.ic_close_white_24dp)
}
fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode {
@@ -55,8 +55,6 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
actionMode.finish()
}
setNavigationIcon(R.drawable.ic_close_white_24dp)
open()
return actionMode

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.view
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
@@ -35,6 +36,7 @@ import android.text.style.ClickableSpan
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar
@@ -166,7 +168,17 @@ fun View.updateLockPaddingLeft() {
))
}
fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) {
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->
Toast.makeText(this, errorId, Toast.LENGTH_LONG).show()
} ?: result.message?.let { message ->
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
}
}
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.errorId?.let { errorId ->
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show()

View File

@@ -4,8 +4,12 @@ import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
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.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
class DatabaseFileViewModel(application: Application) : AndroidViewModel(application) {
@@ -15,6 +19,33 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(application.applicationContext)
}
val isDefaultDatabase: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
fun checkIfIsDefaultDatabase(databaseUri: Uri) {
IOActionTask(
{
(UriUtil.parse(PreferencesUtil.getDefaultDatabasePath(getApplication<App>().applicationContext))
== databaseUri)
},
{
isDefaultDatabase.value = it
}
).execute()
}
fun removeDefaultDatabase() {
IOActionTask(
{
PreferencesUtil.saveDefaultDatabasePath(getApplication<App>().applicationContext,
null)
},
{
}
).execute()
}
val databaseFileLoaded: MutableLiveData<DatabaseFile> by lazy {
MutableLiveData<DatabaseFile>()
}

View File

@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.viewmodels
import android.app.Application
import android.app.backup.BackupManager
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
@@ -42,11 +41,8 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
fun setDefaultDatabase(databaseFile: DatabaseFile?) {
IOActionTask(
{
val context = getApplication<App>().applicationContext
UriUtil.parse(PreferencesUtil.getDefaultDatabasePath(context))
PreferencesUtil.saveDefaultDatabasePath(context, databaseFile?.databaseUri)
val backupManager = BackupManager(context)
backupManager.dataChanged()
PreferencesUtil.saveDefaultDatabasePath(getApplication<App>().applicationContext,
databaseFile?.databaseUri)
},
{
checkDefaultDatabase()

View File

@@ -23,7 +23,6 @@ import android.content.Context
import android.net.Uri
import android.text.format.Formatter
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
import java.io.Serializable
import java.text.DateFormat
@@ -58,7 +57,11 @@ class FileDatabaseInfo : Serializable {
}
private set
fun getModificationString(): String? {
fun getLastModification(): Long? {
return documentFile?.lastModified()
}
fun getLastModificationString(): String? {
return documentFile?.lastModified()?.let {
if (it != 0L) {
DateFormat.getDateTimeInstance()
@@ -69,6 +72,10 @@ class FileDatabaseInfo : Serializable {
}
}
fun getSize(): Long? {
return documentFile?.length()
}
fun getSizeString(): String? {
return documentFile?.let {
Formatter.formatFileSize(context, it.length())

View File

@@ -129,7 +129,7 @@ void throwExceptionF(JNIEnv *env, jclass exception, const char *format, ...) {
JNIEXPORT jbyteArray
JNICALL Java_com_kunzisoft_keepass_crypto_keyDerivation_Argon2Native_nTransformMasterKey(JNIEnv *env,
jobject this, jbyteArray password, jbyteArray salt, jint parallelism, jint memory,
jobject this, jint type, jbyteArray password, jbyteArray salt, jint parallelism, jint memory,
jint iterations, jbyteArray secretKey, jbyteArray associatedData, jint version) {
argon2_context context;
@@ -169,7 +169,7 @@ JNICALL Java_com_kunzisoft_keepass_crypto_keyDerivation_Argon2Native_nTransformM
context.flags = ARGON2_DEFAULT_FLAGS;
context.version = (uint32_t) version;
int argonResult = argon2_ctx(&context, Argon2_d);
int argonResult = argon2_ctx(&context, (argon2_type) type);
jbyteArray result;
if (argonResult != ARGON2_OK) {

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