mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'develop' into feature/Image_Viewer
This commit is contained in:
56
CHANGELOG
56
CHANGELOG
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,29 +292,30 @@ 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 ->
|
||||
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
|
||||
// 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) {
|
||||
@@ -333,27 +335,20 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { 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)
|
||||
|
||||
@@ -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,10 +440,14 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||
if (oldField.name.equals(newField.name, true)) {
|
||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||
} else {
|
||||
verifyNameField(newField) {
|
||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
||||
entryEditFragment?.removeExtraField(oldField)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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) {
|
||||
|
||||
@@ -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,20 +230,19 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
if (resultException is DuplicateUuidDatabaseException) {
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var masterPassword: String? = null
|
||||
var keyFileUri: 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)
|
||||
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||
}
|
||||
@@ -241,14 +250,20 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
masterPassword,
|
||||
keyFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEntity,
|
||||
true)
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
@@ -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,14 +648,8 @@ 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!!,
|
||||
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
||||
readOnlyEducationPerformed,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
@@ -671,7 +658,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||
if (readOnly) {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) { _, _ -> }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,23 +373,39 @@ 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()
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class App : MultiDexApplication() {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,87 @@
|
||||
*/
|
||||
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) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
@@ -41,6 +109,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun containsCipherDatabase(databaseUri: Uri,
|
||||
contains: (Boolean) -> Unit) {
|
||||
@@ -51,10 +120,15 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
|
||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
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)
|
||||
@@ -67,9 +141,16 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
@@ -79,8 +160,12 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
attachService {
|
||||
mBinder?.deleteAll()
|
||||
}
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteAll()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,12 @@ class IOActionTask<T>(
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val asyncResult: Deferred<T?> = async {
|
||||
try {
|
||||
action.invoke()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||
|
||||
@@ -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?)
|
||||
@@ -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,57 +116,139 @@ 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 ->
|
||||
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
entriesInfo.forEach {
|
||||
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||
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,
|
||||
responseBuilder.build())
|
||||
response)
|
||||
setResultOk = true
|
||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!setResultOk) {
|
||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to start an activity with an Autofill for result
|
||||
*/
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
actionIfCypherInit.invoke(
|
||||
AdvancedUnlockCryptoPrompt(
|
||||
cipher,
|
||||
R.string.advanced_unlock_prompt_store_credential_title,
|
||||
R.string.advanced_unlock_prompt_store_credential_message,
|
||||
isDeviceCredentialOperation(), isBiometricOperation())
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
|
||||
actionIfCypherInit.invoke(biometricPrompt,
|
||||
cryptoObject,
|
||||
promptInfoStoreCredential)
|
||||
}
|
||||
} 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))
|
||||
actionIfCypherInit.invoke(
|
||||
AdvancedUnlockCryptoPrompt(
|
||||
cipher,
|
||||
R.string.advanced_unlock_prompt_extract_credential_title,
|
||||
null,
|
||||
isDeviceCredentialOperation(), isBiometricOperation())
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
|
||||
actionIfCypherInit.invoke(biometricPrompt,
|
||||
cryptoObject,
|
||||
promptInfoExtractCredential)
|
||||
}
|
||||
} 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,8 +413,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
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
|
||||
@@ -374,30 +423,41 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
|| 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 (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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,43 +128,36 @@ 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 open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
return null
|
||||
}
|
||||
|
||||
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
|
||||
|
||||
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null && !containsKeyFile)
|
||||
return false
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
try {
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
|
||||
if (randomStream == null) {
|
||||
throw ArcFourDatabaseException()
|
||||
} 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,10 +1038,12 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
return xpp.safeNextText()
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readBase64String(xpp: XmlPullParser): ByteArray {
|
||||
|
||||
//readNextNode = false;
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
|
||||
if (xpp.attributeCount > 0) {
|
||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
|
||||
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
||||
val plainText = ByteArray(data.size)
|
||||
randomStream?.processBytes(data, 0, data.size, plainText, 0)
|
||||
@@ -1035,18 +1051,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
try {
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
if (randomStream == null) {
|
||||
throw DatabaseOutputException("Invalid random cipher")
|
||||
} 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 ->
|
||||
binary.getInputDataStream().use { inputStream ->
|
||||
inputStream.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)
|
||||
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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
otpModel?.let {
|
||||
return OtpElement(it).token
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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],
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kunzisoft.keepass.notifications
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -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?) {
|
||||
|
||||
@@ -103,6 +103,6 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen)
|
||||
fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean = false)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ->
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
84
app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt
Normal file
84
app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt
Normal file
14
app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt
Normal 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) }
|
||||
}
|
||||
@@ -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
|
||||
expiresDateView.text = if (isExpires) {
|
||||
expiryTime.getDateTimeString(resources)
|
||||
} else {
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
|
||||
fun assignExpiresDate(date: DateInstant) {
|
||||
assignExpiresDate(date.getDateTimeString(resources))
|
||||
}
|
||||
|
||||
fun assignExpiresDate(constString: String) {
|
||||
expiresDateView.text = constString
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user