mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8c7acf7ce | ||
|
|
17a356ae76 | ||
|
|
bd847e632d | ||
|
|
2bfb9b048d | ||
|
|
dc40b50b65 | ||
|
|
3e2271e596 | ||
|
|
4b4fd2a11d | ||
|
|
23468290df | ||
|
|
a276f6aa06 | ||
|
|
f2a58361a1 | ||
|
|
271023b528 | ||
|
|
e1771ca249 | ||
|
|
ca4f4bd151 | ||
|
|
d81454d618 | ||
|
|
fb43c1c624 | ||
|
|
9e060f878d | ||
|
|
bd9c21ee8a | ||
|
|
3e6d40e8da | ||
|
|
79683cb3fc | ||
|
|
52a2090a31 | ||
|
|
3dfe4ace7b | ||
|
|
bd0d17b134 | ||
|
|
6b0ccc1780 | ||
|
|
d75ac4b825 | ||
|
|
b60d610d02 | ||
|
|
f7a5c5d0ea | ||
|
|
28f79aec11 | ||
|
|
778d963fbf | ||
|
|
a765bc84e7 | ||
|
|
804ecc1baa | ||
|
|
d331c3dc03 | ||
|
|
7010d2f86a | ||
|
|
b1d6117eb2 | ||
|
|
f3b814388d | ||
|
|
b62996a57c | ||
|
|
a49e056f02 | ||
|
|
a6dece16bf | ||
|
|
8e3ddd64d2 | ||
|
|
45a847fa3e | ||
|
|
6b6f03b143 | ||
|
|
5446efca4a | ||
|
|
8d04a7f90b | ||
|
|
626495c19e | ||
|
|
e5a198f524 | ||
|
|
161524843f | ||
|
|
5550e7dea3 | ||
|
|
64f66c290c | ||
|
|
e8925b3c0b | ||
|
|
cf67ce04a8 | ||
|
|
84ee4ca2c7 | ||
|
|
27eb095720 | ||
|
|
d273f21819 | ||
|
|
455fd0cd6d | ||
|
|
c5a8650c81 | ||
|
|
b5f9bbed5e | ||
|
|
e789741090 | ||
|
|
5c6d93bc57 | ||
|
|
697b672038 | ||
|
|
2d9e9c24a8 | ||
|
|
5fb281c800 | ||
|
|
96896c1c42 | ||
|
|
d7052bd9e6 | ||
|
|
8b23932788 | ||
|
|
50912c6966 | ||
|
|
53b51934b9 | ||
|
|
a8a3685965 | ||
|
|
149b67e28b | ||
|
|
a83032bffa | ||
|
|
a5d3a153bf | ||
|
|
4210c155eb | ||
|
|
4bf110a9b1 | ||
|
|
50f2684500 | ||
|
|
e95424b8f9 | ||
|
|
8462882707 | ||
|
|
a5c8d25f64 | ||
|
|
689ce2f9b3 | ||
|
|
54246533ac | ||
|
|
66e4b0fe47 | ||
|
|
3e8ae3e2e3 | ||
|
|
d856ef3772 | ||
|
|
5727880ac7 | ||
|
|
ec4302a780 | ||
|
|
d4203598a1 | ||
|
|
a278c8c718 | ||
|
|
faf5f4b51a | ||
|
|
b2f503b326 | ||
|
|
beb5484bf6 | ||
|
|
062a9852e5 | ||
|
|
9a6a709746 | ||
|
|
5ab3cf985a | ||
|
|
e18b3436c9 | ||
|
|
fcb1b5ae6b |
18
CHANGELOG
18
CHANGELOG
@@ -1,6 +1,20 @@
|
||||
KeePassDX(2.9.7)
|
||||
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
|
||||
* Remove write permission since Android 10
|
||||
|
||||
KeePassDX(2.9.6)
|
||||
* Fix KeyFile bug #820
|
||||
|
||||
@@ -5,15 +5,15 @@ 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 = 51
|
||||
versionName = "2.9.7"
|
||||
versionCode = 53
|
||||
versionName = "2.9.9"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -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')
|
||||
|
||||
@@ -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,6 +39,7 @@ 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
|
||||
@@ -53,7 +54,9 @@ 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_RELOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
@@ -151,6 +154,10 @@ class EntryActivity : LockingActivity() {
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Close the current activity
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
@@ -199,8 +206,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
|
||||
@@ -232,23 +238,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 =
|
||||
@@ -278,11 +282,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
|
||||
@@ -292,44 +294,46 @@ class EntryActivity : LockingActivity() {
|
||||
null
|
||||
}
|
||||
}
|
||||
entryContentsView?.assignPassword(entry.password,
|
||||
entryContentsView?.assignPassword(entryInfo.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||
View.OnClickListener {
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
})
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.assignNotes(entry.notes)
|
||||
entryContentsView?.assignURL(entryInfo.url)
|
||||
entryContentsView?.assignNotes(entryInfo.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
entry.getExtraFields().forEach { field ->
|
||||
entryInfo.customFields.forEach { field ->
|
||||
val label = field.name
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
// OTP field is already managed in dedicated view
|
||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,24 +341,16 @@ class EntryActivity : LockingActivity() {
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
||||
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
||||
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
||||
if (entry.expires) {
|
||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
||||
} else {
|
||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||
}
|
||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||
entryContentsView?.assignModificationDate(entryInfo.modificationTime)
|
||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||
|
||||
// Manage history
|
||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||
@@ -369,8 +365,6 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@@ -408,6 +402,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 {
|
||||
@@ -501,6 +498,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
|
||||
@@ -49,6 +48,7 @@ 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
|
||||
@@ -61,6 +61,7 @@ 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_RELOAD_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
|
||||
@@ -335,6 +336,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Close the current activity
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
@@ -361,7 +366,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))
|
||||
}
|
||||
}
|
||||
@@ -479,8 +484,12 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||
verifyNameField(newField) {
|
||||
if (oldField.name.equals(newField.name, true)) {
|
||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||
} else {
|
||||
verifyNameField(newField) {
|
||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,13 +619,7 @@ 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)
|
||||
|
||||
MenuUtil.contributionMenuInflater(menuInflater, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -673,9 +676,6 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||
}
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
@@ -909,7 +909,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)) {
|
||||
@@ -917,7 +917,7 @@ class EntryEditActivity : LockingActivity(),
|
||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
intent,
|
||||
assistStructure,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -48,6 +47,7 @@ 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
|
||||
@@ -501,11 +501,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
|
||||
@@ -52,6 +51,7 @@ 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
|
||||
@@ -70,6 +70,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
||||
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_RELOAD_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
|
||||
@@ -228,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 {
|
||||
@@ -342,6 +343,12 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Reload the current activity
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
}
|
||||
|
||||
coordinatorLayout?.showActionError(result)
|
||||
@@ -665,7 +672,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))
|
||||
}
|
||||
}
|
||||
@@ -878,6 +885,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
|
||||
@@ -1003,6 +1012,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
|
||||
@@ -1310,14 +1323,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)
|
||||
}
|
||||
}
|
||||
@@ -1434,21 +1447,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()
|
||||
|
||||
@@ -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
|
||||
@@ -49,6 +48,7 @@ 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.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
@@ -720,7 +720,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
@@ -838,13 +838,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -902,11 +902,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,13 @@ 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 AutofillComponent(assistStructure,
|
||||
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -68,26 +82,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 +98,12 @@ object AutofillHelper {
|
||||
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
@@ -108,44 +112,116 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
|
||||
if (positionItem <= maxSuggestion-1
|
||||
&& inlinePresentationSpecs.size > positionItem) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(context,
|
||||
0,
|
||||
Intent(context, AutofillSettingsActivity::class.java),
|
||||
0)
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(entryInfo.title)
|
||||
setSubtitle(entryInfo.username)
|
||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, entryInfo)?.let { icon ->
|
||||
setEndIcon(icon.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val packageName = context.packageName
|
||||
parseResult.webDomain?.let { webDomain ->
|
||||
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, webDomain)
|
||||
})
|
||||
} ?: kotlin.run {
|
||||
parseResult.applicationId?.let { applicationId ->
|
||||
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
|
||||
setTextViewText(R.id.autofill_app_id_text, applicationId)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add inline suggestion for new IME and dataset
|
||||
entriesInfo.forEachIndexed { index, entryInfo ->
|
||||
val inlinePresentation = inlineSuggestionsRequest?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation))
|
||||
}
|
||||
return responseBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for one entry
|
||||
*/
|
||||
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
|
||||
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) {
|
||||
buildResponseAndSetResult(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Autofill response for many entry
|
||||
*/
|
||||
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||
fun buildResponseAndSetResult(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||
if (entriesInfo.isEmpty()) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
var setResultOk = false
|
||||
activity.intent?.extras?.let { extras ->
|
||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
entriesInfo.forEach {
|
||||
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||
mReplyIntent.putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
responseBuilder.build())
|
||||
setResultOk = true
|
||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
}
|
||||
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
val response = buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
|
||||
if (inlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||
mReplyIntent.putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
response)
|
||||
setResultOk = true
|
||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||
}
|
||||
if (!setResultOk) {
|
||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
if (!setResultOk) {
|
||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,10 +231,16 @@ object AutofillHelper {
|
||||
*/
|
||||
fun startActivityForAutofillResult(activity: Activity,
|
||||
intent: Intent,
|
||||
assistStructure: AssistStructure,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||
autofillComponent.inlineSuggestionsRequest?.let {
|
||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||
}
|
||||
@@ -192,4 +274,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,8 +172,45 @@ class KeeAutofillService : AutofillService() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Build inline presentation
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
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)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
} else {
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
}
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
@@ -190,6 +250,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
|
||||
|
||||
@@ -554,18 +554,20 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
readOnlyEducationPerformed: Boolean,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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,
|
||||
@@ -47,7 +46,7 @@ class CreateDatabaseRunnable(context: Context,
|
||||
createData(mDatabaseUri, databaseName, rootName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||
setError(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ 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,
|
||||
@@ -47,7 +46,7 @@ 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() {
|
||||
@@ -59,9 +58,6 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
catch (e: DuplicateUuidDatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
@@ -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,6 +37,7 @@ 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.model.SnapFileDatabaseInfo
|
||||
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
|
||||
@@ -44,6 +47,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
||||
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_RELOAD_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
|
||||
@@ -84,6 +88,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 +106,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 +167,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 +236,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
fun unregisterProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder = null
|
||||
|
||||
@@ -264,6 +295,13 @@ 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?,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.DuplicateUuidDatabaseException
|
||||
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
|
||||
@@ -330,29 +327,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 +353,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 +364,90 @@ class Database {
|
||||
|
||||
this.mSearchHelper = SearchHelper()
|
||||
loaded = true
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} finally {
|
||||
databaseInputStream?.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
|
||||
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
|
||||
keyfile?.let {
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
||||
}
|
||||
|
||||
// Read database stream for the first time
|
||||
readDatabaseStream(contentResolver, uri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(cacheDirectory)
|
||||
.openDatabase(databaseInputStream,
|
||||
password,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater,
|
||||
fixDuplicateUUID)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(cacheDirectory)
|
||||
.openDatabase(databaseInputStream,
|
||||
password,
|
||||
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 FileNotFoundDatabaseException()
|
||||
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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +469,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 +574,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 +587,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
|
||||
|
||||
@@ -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.hexStringToByteArray
|
||||
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.w3c.dom.Node
|
||||
import org.w3c.dom.Text
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -180,7 +182,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()) {
|
||||
@@ -198,7 +201,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
CompressionAlgorithm.None -> {
|
||||
decompressAllBinaries()
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,36 +382,79 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
try {
|
||||
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
||||
} catch (e : ParserConfigurationException) {
|
||||
Log.e(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)", e)
|
||||
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
|
||||
}
|
||||
|
||||
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
||||
val doc = documentBuilder.parse(keyInputStream)
|
||||
|
||||
var xmlKeyFileVersion = 1F
|
||||
|
||||
val docElement = doc.documentElement
|
||||
if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) {
|
||||
val keyFileChildNodes = docElement.childNodes
|
||||
// <KeyFile> Root node
|
||||
if (docElement == null
|
||||
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val children = docElement.childNodes
|
||||
if (children.length < 2) {
|
||||
if (keyFileChildNodes.length < 2)
|
||||
return null
|
||||
}
|
||||
|
||||
for (i in 0 until children.length) {
|
||||
val child = children.item(i)
|
||||
|
||||
if (child.nodeName.equals(KeyElementName, ignoreCase = true)) {
|
||||
val keyChildren = child.childNodes
|
||||
for (j in 0 until keyChildren.length) {
|
||||
val keyChild = keyChildren.item(j)
|
||||
if (keyChild.nodeName.equals(KeyDataElementName, ignoreCase = true)) {
|
||||
val children2 = keyChild.childNodes
|
||||
for (k in 0 until children2.length) {
|
||||
val text = children2.item(k)
|
||||
if (text.nodeType == Node.TEXT_NODE) {
|
||||
val txt = text as Text
|
||||
return Base64.decode(txt.nodeValue, BASE_64_FLAG)
|
||||
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
|
||||
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
|
||||
// <Meta>
|
||||
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
|
||||
val metaChildNodes = keyFileChildNode.childNodes
|
||||
for (metaChildPosition in 0 until metaChildNodes.length) {
|
||||
val metaChildNode = metaChildNodes.item(metaChildPosition)
|
||||
// <Version>
|
||||
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
|
||||
val versionChildNodes = metaChildNode.childNodes
|
||||
for (versionChildPosition in 0 until versionChildNodes.length) {
|
||||
val versionChildNode = versionChildNodes.item(versionChildPosition)
|
||||
if (versionChildNode.nodeType == Node.TEXT_NODE) {
|
||||
val versionText = versionChildNode.textContent.removeSpaceChars()
|
||||
try {
|
||||
xmlKeyFileVersion = versionText.toFloat()
|
||||
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// <Key>
|
||||
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
|
||||
val keyChildNodes = keyFileChildNode.childNodes
|
||||
for (keyChildPosition in 0 until keyChildNodes.length) {
|
||||
val keyChildNode = keyChildNodes.item(keyChildPosition)
|
||||
// <Data>
|
||||
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
|
||||
var hashString : String? = null
|
||||
if (keyChildNode.hasAttributes()) {
|
||||
val dataNodeAttributes = keyChildNode.attributes
|
||||
hashString = dataNodeAttributes
|
||||
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
|
||||
}
|
||||
val dataChildNodes = keyChildNode.childNodes
|
||||
for (dataChildPosition in 0 until dataChildNodes.length) {
|
||||
val dataChildNode = dataChildNodes.item(dataChildPosition)
|
||||
if (dataChildNode.nodeType == Node.TEXT_NODE) {
|
||||
val dataString = dataChildNode.textContent.removeSpaceChars()
|
||||
when (xmlKeyFileVersion) {
|
||||
1F -> {
|
||||
// No hash in KeyFile XML version 1
|
||||
}
|
||||
2F -> {
|
||||
if (hashString != null
|
||||
&& checkKeyFileHash(dataString, hashString))
|
||||
Log.i(TAG, "Successful key file hash check.")
|
||||
else
|
||||
Log.e(TAG, "Unable to check the hash of the key file.")
|
||||
}
|
||||
}
|
||||
return Base64.decode(dataString, BASE_64_FLAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,10 +464,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(data.hexStringToByteArray())
|
||||
.copyOfRange(0, 4)
|
||||
.toHexString()
|
||||
success = dataDigest == hash
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
override fun newGroupId(): NodeIdUUID {
|
||||
var newId: NodeIdUUID
|
||||
do {
|
||||
@@ -634,11 +697,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,10 @@ 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 java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
@@ -124,42 +127,30 @@ 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 32 bits key file
|
||||
if (keyData.size == 32) {
|
||||
return keyData
|
||||
}
|
||||
|
||||
when (keyData.size.toLong()) {
|
||||
32L -> return keyData
|
||||
64L -> try {
|
||||
return hexStringToByteArray(String(keyData))
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
}
|
||||
// Check XML key file
|
||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||
if (xmlKeyByteArray != null) {
|
||||
return xmlKeyByteArray
|
||||
}
|
||||
|
||||
val messageDigest: MessageDigest
|
||||
// Hash file as binary data
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
return MessageDigest.getInstance("SHA-256").digest(keyData)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IOException("SHA-256 not supported")
|
||||
}
|
||||
|
||||
try {
|
||||
messageDigest.update(keyData)
|
||||
} catch (e: Exception) {
|
||||
println(e.toString())
|
||||
}
|
||||
|
||||
return messageDigest.digest()
|
||||
}
|
||||
|
||||
protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray?
|
||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null && !containsKeyFile)
|
||||
@@ -391,16 +382,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)
|
||||
}
|
||||
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
|
||||
if (randomStream == null) {
|
||||
throw ArcFourDatabaseException()
|
||||
try {
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
}
|
||||
|
||||
readDocumentStreamed(createPullParser(inputStreamXml))
|
||||
@@ -436,8 +452,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
val strData = readString(xpp)
|
||||
if (strData.isNotEmpty()) {
|
||||
customIconData = Base64.decode(strData, BASE_64_FLAG)
|
||||
} else {
|
||||
assert(false)
|
||||
}
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
@@ -958,7 +972,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
// Create empty binary if not retrieved in pool
|
||||
if (binaryRetrieve == null) {
|
||||
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
||||
compression = false, protection = true, binaryPoolId = id)
|
||||
compression = false, protection = false, binaryPoolId = id)
|
||||
}
|
||||
return binaryRetrieve
|
||||
}
|
||||
@@ -1024,29 +1038,20 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
return xpp.safeNextText()
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readBase64String(xpp: XmlPullParser): ByteArray {
|
||||
|
||||
//readNextNode = false;
|
||||
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
||||
val plainText = ByteArray(data.size)
|
||||
randomStream?.processBytes(data, 0, data.size, plainText, 0)
|
||||
return plainText
|
||||
}
|
||||
return ByteArray(0)
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun readProtectedBase64String(xpp: XmlPullParser): ByteArray? {
|
||||
//(xpp.getEventType() == XmlPullParser.START_TAG);
|
||||
|
||||
if (xpp.attributeCount > 0) {
|
||||
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
|
||||
if (protect != null && protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)) {
|
||||
return readBase64String(xpp)
|
||||
Base64.decode(xpp.safeNextText(), BASE_64_FLAG)?.let { data ->
|
||||
val plainText = ByteArray(data.size)
|
||||
randomStream?.processBytes(data, 0, data.size, plainText, 0)
|
||||
return plainText
|
||||
}
|
||||
return ByteArray(0)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
if (randomStream == null) {
|
||||
throw DatabaseOutputException("Invalid random cipher")
|
||||
try {
|
||||
randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
} catch (e: Exception) {
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
|
||||
if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) {
|
||||
@@ -420,41 +463,56 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
writeObject(name, String(Base64.encode(data, BASE_64_FLAG)))
|
||||
}
|
||||
|
||||
/*
|
||||
// Normally used by a single entry but obsolete because binaries are in meta tag with kdbx3.1-
|
||||
// or in file header with kdbx4
|
||||
// binary.isProtected attribute is not used to create the XML
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeBinary(binary : BinaryAttachment) {
|
||||
val binaryLength = binary.length()
|
||||
if (binaryLength > 0) {
|
||||
private fun writeEntryBinary(binary : BinaryAttachment) {
|
||||
if (binary.length() > 0) {
|
||||
if (binary.isProtected) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||
|
||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val encoded = ByteArray(buffer.size)
|
||||
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
||||
val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
binary.getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val encoded = ByteArray(buffer.size)
|
||||
randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
|
||||
xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (binary.isCompressed) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||
}
|
||||
// Write the XML
|
||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
binary.getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Only uses with kdbx3.1 to write binaries in meta tag
|
||||
// With kdbx4, don't use this method because binaries are in header file
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeMetaBinaries() {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||
|
||||
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
|
||||
// Use indexes because necessarily (binary header ref is the order)
|
||||
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||
writeBinary(keyBinary.binary)
|
||||
val binary = keyBinary.binary
|
||||
if (binary.length() > 0) {
|
||||
if (binary.isCompressed) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||
}
|
||||
// Write the XML
|
||||
binary.getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
|
||||
}
|
||||
}
|
||||
}
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
|
||||
@@ -523,13 +581,11 @@ 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -95,10 +101,6 @@ class EntryInfo : Parcelable {
|
||||
return customFields.lastOrNull { it.name == label } != null
|
||||
}
|
||||
|
||||
fun isAutoGeneratedField(field: Field): Boolean {
|
||||
return field.name == OTP_TOKEN_FIELD
|
||||
}
|
||||
|
||||
fun getGeneratedFieldValue(label: String): String {
|
||||
if (label == OTP_TOKEN_FIELD) {
|
||||
otpModel?.let {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ 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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -22,9 +22,8 @@ package com.kunzisoft.keepass.notifications
|
||||
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,7 @@ 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.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
@@ -47,6 +47,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 +66,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
private var mAllowFinishAction = AtomicBoolean()
|
||||
private var mActionRunning = false
|
||||
|
||||
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
|
||||
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,31 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
fun checkDatabaseInfo() {
|
||||
mDatabase.fileUri?.let {
|
||||
val previousDatabaseInfo = mSnapFileDatabaseInfo
|
||||
val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
|
||||
FileDatabaseInfo(applicationContext, it))
|
||||
if (previousDatabaseInfo != null) {
|
||||
if (previousDatabaseInfo != lastFileDatabaseInfo) {
|
||||
Log.i(TAG, "Database file modified " +
|
||||
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
||||
// Call listener to indicate a change in database info
|
||||
mDatabaseInfoListeners.forEach { listener ->
|
||||
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDatabaseInfo() {
|
||||
mDatabase.fileUri?.let {
|
||||
mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo(
|
||||
FileDatabaseInfo(applicationContext, it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
return mActionTaskBinder
|
||||
@@ -138,6 +180,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)
|
||||
@@ -193,6 +236,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
} finally {
|
||||
removeIntentData(intent)
|
||||
// Save the database info after performing action
|
||||
saveDatabaseInfo()
|
||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) {
|
||||
if (!mDatabase.loaded) {
|
||||
@@ -214,7 +259,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 +295,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 +306,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
|
||||
@@ -342,9 +392,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
* Execute action with a coroutine
|
||||
*/
|
||||
private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater,
|
||||
onPreExecute: () -> Unit,
|
||||
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
|
||||
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
|
||||
onPreExecute: () -> Unit,
|
||||
onExecute: (ProgressTaskUpdater?) -> ActionRunnable?,
|
||||
onPostExecute: (result: ActionRunnable.Result) -> Unit) {
|
||||
mAllowFinishAction.set(false)
|
||||
|
||||
TimeoutHelper.temporarilyDisableTimeout()
|
||||
@@ -465,6 +515,17 @@ 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)
|
||||
@@ -770,6 +831,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"
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -316,7 +365,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 +431,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -552,6 +552,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
|
||||
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly)
|
||||
true
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
return true
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Check the time lock before launching settings
|
||||
|
||||
@@ -436,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
|
||||
@@ -37,6 +36,7 @@ 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.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
|
||||
@@ -95,12 +95,28 @@ open class SettingsActivity
|
||||
backupManager = BackupManager(this)
|
||||
|
||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||
// Call result in fragment
|
||||
(supportFragmentManager
|
||||
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
||||
?.onProgressDialogThreadResult(actionTask, result)
|
||||
when (actionTask) {
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Reload the current activity
|
||||
startActivity(intent)
|
||||
finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
else -> {
|
||||
// Call result in fragment
|
||||
(supportFragmentManager
|
||||
.findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?)
|
||||
?.onProgressDialogThreadResult(actionTask, result)
|
||||
coordinatorLayout?.showActionError(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,25 +209,33 @@ 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)
|
||||
// To reload the current screen
|
||||
intent.putExtra(FRAGMENT_ARG, key.name)
|
||||
hideOrShowLockButton(key)
|
||||
}
|
||||
|
||||
override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) {
|
||||
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) {
|
||||
@@ -226,6 +250,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)
|
||||
|
||||
@@ -138,5 +138,5 @@ fun Context.closeDatabase() {
|
||||
cancelAll()
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||
}
|
||||
26
app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt
Normal file
26
app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
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 String.hexStringToByteArray(): ByteArray {
|
||||
val len = this.length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] = ((Character.digit(this[i], 16) shl 4)
|
||||
+ Character.digit(this[i + 1], 16)).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
|
||||
}
|
||||
@@ -67,7 +67,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
private val creationDateView: TextView
|
||||
private val modificationDateView: TextView
|
||||
private val lastAccessDateView: TextView
|
||||
private val expiresImageView: ImageView
|
||||
private val expiresDateView: TextView
|
||||
|
||||
@@ -117,7 +116,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
|
||||
creationDateView = findViewById(R.id.entry_created)
|
||||
modificationDateView = findViewById(R.id.entry_modified)
|
||||
lastAccessDateView = findViewById(R.id.entry_accessed)
|
||||
expiresImageView = findViewById(R.id.entry_expires_image)
|
||||
expiresDateView = findViewById(R.id.entry_expires_date)
|
||||
|
||||
@@ -258,20 +256,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
modificationDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignLastAccessDate(date: DateInstant) {
|
||||
lastAccessDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun setExpires(isExpires: Boolean) {
|
||||
fun setExpires(isExpires: Boolean, expiryTime: DateInstant) {
|
||||
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun assignExpiresDate(date: DateInstant) {
|
||||
assignExpiresDate(date.getDateTimeString(resources))
|
||||
}
|
||||
|
||||
fun assignExpiresDate(constString: String) {
|
||||
expiresDateView.text = constString
|
||||
expiresDateView.text = if (isExpires) {
|
||||
expiryTime.getDateTimeString(resources)
|
||||
} else {
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignUUID(uuid: UUID) {
|
||||
@@ -279,7 +270,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||
}
|
||||
|
||||
|
||||
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
|
||||
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
|
||||
// Hidden style for custom fields
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
7
app/src/main/res/drawable/ic_reload_white_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_reload_white_24dp.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24" >
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
|
||||
</vector>
|
||||
@@ -46,9 +46,9 @@
|
||||
android:id="@+id/entry_extra_field_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignTop="@+id/entry_extra_field_value_container"
|
||||
android:src="@drawable/ic_more_white_24dp"
|
||||
android:contentDescription="@string/menu_edit"
|
||||
style="@style/KeepassDXStyle.ImageButton.Simple"/>
|
||||
|
||||
@@ -175,12 +175,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/entry_accessed"
|
||||
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/entry_accessed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
|
||||
|
||||
<!-- Expires -->
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/keyfile_open_button">
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/keyfile_name"
|
||||
@@ -33,16 +33,4 @@
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="1"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/keyfile_open_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="@string/content_description_open_file"
|
||||
android:focusable="true"
|
||||
android:background="@drawable/background_item_selection"
|
||||
android:src="@drawable/ic_folder_white_24dp"
|
||||
style="@style/KeepassDXStyle.ImageButton.Simple" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -22,6 +22,6 @@
|
||||
<item android:id="@+id/menu_contribute"
|
||||
android:icon="@drawable/ic_heart_white_24dp"
|
||||
android:title="@string/contribute"
|
||||
android:orderInCategory="95"
|
||||
android:orderInCategory="99"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -24,4 +24,9 @@
|
||||
android:title="@string/menu_save_database"
|
||||
android:orderInCategory="95"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item android:id="@+id/menu_reload_database"
|
||||
android:icon="@drawable/ic_reload_white_24dp"
|
||||
android:title="@string/menu_reload_database"
|
||||
android:orderInCategory="96"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
12
app/src/main/res/values-bg/strings.xml
Normal file
12
app/src/main/res/values-bg/strings.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="feedback">Обратна връзка</string>
|
||||
<string name="encryption_algorithm">Алгоритъм за криптиране</string>
|
||||
<string name="encryption">Криптиране</string>
|
||||
<string name="security">Сигурност</string>
|
||||
<string name="master_key">Главен ключ</string>
|
||||
<string name="add_group">Добави група</string>
|
||||
<string name="edit_entry">Редактирай</string>
|
||||
<string name="add_entry">Добави</string>
|
||||
<string name="accept">Приемам</string>
|
||||
</resources>
|
||||
@@ -19,12 +19,12 @@
|
||||
Czech translation by Jan Vaněk
|
||||
--><resources>
|
||||
<string name="homepage">Domovská stránka</string>
|
||||
<string name="about_description">Androidová verze správce hesel KeePass</string>
|
||||
<string name="about_description">Implementace správce hesel KeePass pro Android</string>
|
||||
<string name="accept">Přijmout</string>
|
||||
<string name="add_entry">Přidat záznam</string>
|
||||
<string name="add_group">Přidat skupinu</string>
|
||||
<string name="encryption_algorithm">Šifrovací algoritmus</string>
|
||||
<string name="app_timeout">Časový limit aplikace</string>
|
||||
<string name="app_timeout">Časový limit</string>
|
||||
<string name="app_timeout_summary">Doba nečinnosti, po které se aplikace zamkne</string>
|
||||
<string name="application">Aplikace</string>
|
||||
<string name="menu_app_settings">Nastavení aplikace</string>
|
||||
@@ -35,16 +35,16 @@
|
||||
<string name="clipboard_error">Některá zařízení nedovolují aplikacím používat schránku.</string>
|
||||
<string name="clipboard_error_clear">Nelze vyprázdnit schránku</string>
|
||||
<string name="clipboard_timeout">Časový limit schránky</string>
|
||||
<string name="clipboard_timeout_summary">Doba uchování ve schránce</string>
|
||||
<string name="clipboard_timeout_summary">Doba uchování ve schránce (je-li podporována zařízením)</string>
|
||||
<string name="select_to_copy">Vyberte zkopírovat %1$s do schránky</string>
|
||||
<string name="retrieving_db_key">Načítání klíče databáze…</string>
|
||||
<string name="retrieving_db_key">Načítám klíč databáze…</string>
|
||||
<string name="database">Databáze</string>
|
||||
<string name="decrypting_db">Dešifrování obsahu databáze…</string>
|
||||
<string name="decrypting_db">Dešifruji obsah databáze…</string>
|
||||
<string name="default_checkbox">Použít jako výchozí databázi</string>
|
||||
<string name="digits">Číslice</string>
|
||||
<string name="select_database_file">Otevřít existující databázi</string>
|
||||
<string name="entry_accessed">Poslední přístup</string>
|
||||
<string name="entry_cancel">Storno</string>
|
||||
<string name="entry_cancel">Zrušit</string>
|
||||
<string name="entry_notes">Poznámky</string>
|
||||
<string name="entry_confpassword">Potvrďte heslo</string>
|
||||
<string name="entry_created">Vytvořeno</string>
|
||||
@@ -55,27 +55,27 @@
|
||||
<string name="entry_password">Heslo</string>
|
||||
<string name="save">Uložit</string>
|
||||
<string name="entry_title">Název</string>
|
||||
<string name="entry_url">URL adresa</string>
|
||||
<string name="entry_url">URL</string>
|
||||
<string name="entry_user_name">Uživatelské jméno</string>
|
||||
<string name="error_arc4">Arcfour proudová šifra není podporována.</string>
|
||||
<string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string>
|
||||
<string name="error_file_not_create">Soubor se nedaří vytvořit</string>
|
||||
<string name="error_invalid_db">Databázi nelze číst.</string>
|
||||
<string name="error_file_not_create">Soubor se nepodařilo vytvořit</string>
|
||||
<string name="error_invalid_db">Databázi se nepodařilo načíst.</string>
|
||||
<string name="error_invalid_path">Ujistěte se, že cesta je správná.</string>
|
||||
<string name="error_no_name">Zadejte jméno.</string>
|
||||
<string name="error_nokeyfile">Vyberte soubor s klíčem.</string>
|
||||
<string name="error_out_of_memory">Nedostatek paměti k načtení celé databáze.</string>
|
||||
<string name="error_pass_gen_type">Je třeba zvolit alespoň jeden způsob vytváření hesla.</string>
|
||||
<string name="error_pass_match">Zadaná hesla se neshodují.</string>
|
||||
<string name="error_pass_match">Hesla se neshodují.</string>
|
||||
<string name="error_rounds_too_large">Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648.</string>
|
||||
<string name="error_string_key">Je třeba, aby každý řetězec měl název kolonky.</string>
|
||||
<string name="error_wrong_length">Do pole „Délka“ zadejte celé kladné číslo.</string>
|
||||
<string name="field_name">Název pole</string>
|
||||
<string name="field_value">Hodnota pole</string>
|
||||
<string name="field_name">Název kolonky</string>
|
||||
<string name="field_value">Hodnota kolonky</string>
|
||||
<string name="file_browser">Správce souborů</string>
|
||||
<string name="generate_password">Vytvoř heslo</string>
|
||||
<string name="generate_password">Generovat heslo</string>
|
||||
<string name="hint_conf_pass">Potvrdit heslo</string>
|
||||
<string name="hint_generated_password">Vytvořené heslo</string>
|
||||
<string name="hint_generated_password">Generované heslo</string>
|
||||
<string name="hint_group_name">Název skupiny</string>
|
||||
<string name="hint_keyfile">Soubor s klíčem</string>
|
||||
<string name="hint_length">Délka</string>
|
||||
@@ -83,8 +83,8 @@
|
||||
<string name="password">Heslo</string>
|
||||
<string name="invalid_credentials">Nebylo možno načíst autentizační údaje.</string>
|
||||
<string name="invalid_algorithm">Nesprávný algoritmus.</string>
|
||||
<string name="invalid_db_sig">Nedaří se rozpoznat formát databáze.</string>
|
||||
<string name="keyfile_is_empty">Soubor s klíčem je prázdný.</string>
|
||||
<string name="invalid_db_sig">Nepodařilo se rozpoznat formát databáze.</string>
|
||||
<string name="keyfile_is_empty">Soubor klíče je prázdný.</string>
|
||||
<string name="length">Délka</string>
|
||||
<string name="list_size_title">Velikost položek seznamu</string>
|
||||
<string name="list_size_summary">Velikost textu v seznamu prvků</string>
|
||||
@@ -97,28 +97,28 @@
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="menu_database_settings">Nastavení databáze</string>
|
||||
<string name="menu_delete">Smazat</string>
|
||||
<string name="menu_donate">Podpořit vývoj darem</string>
|
||||
<string name="menu_donate">Přispět darem</string>
|
||||
<string name="menu_edit">Upravit</string>
|
||||
<string name="menu_hide_password">Skrýt heslo</string>
|
||||
<string name="menu_lock">Zamknout databázi</string>
|
||||
<string name="menu_open">Otevřít</string>
|
||||
<string name="menu_search">Hledat</string>
|
||||
<string name="menu_showpass">Ukaž heslo</string>
|
||||
<string name="menu_url">Jít na URL</string>
|
||||
<string name="menu_showpass">Ukázat heslo</string>
|
||||
<string name="menu_url">Přejít na URL</string>
|
||||
<string name="minus">Mínus</string>
|
||||
<string name="never">Nikdy</string>
|
||||
<string name="no_results">Žádné výsledky hledání</string>
|
||||
<string name="no_url_handler">Pro otevření tohoto URL nainstalujte webový prohlížeč.</string>
|
||||
<string name="omit_backup_search_title">Neprohledávat položky v záloze</string>
|
||||
<string name="omit_backup_search_summary">Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání</string>
|
||||
<string name="progress_create">Vytvářím novou databázi…</string>
|
||||
<string name="progress_title">Zpracování…</string>
|
||||
<string name="progress_create">Zakládám novou databázi…</string>
|
||||
<string name="progress_title">Pracuji…</string>
|
||||
<string name="protection">Ochrana</string>
|
||||
<string name="read_only_warning">Ke změně v databázi potřebuje KeePassDX oprávnění pro zápis.</string>
|
||||
<string name="read_only_warning">Ke změně v databáze potřebuje KeePassDX oprávnění pro zápis.</string>
|
||||
<string name="content_description_remove_from_list">Odstranit</string>
|
||||
<string name="encryption_rijndael">Rijndael (AES)</string>
|
||||
<string name="root">Kořen</string>
|
||||
<string name="rounds">Počet šifrovacích průchodů</string>
|
||||
<string name="rounds">Transformační průchody</string>
|
||||
<string name="rounds_explanation">Vyšší počet šifrovacích průchodů zvýší odolnost proti útoku zkoušením všech možných hesel, ale může výrazně zpomalit načítání a ukládání.</string>
|
||||
<string name="saving_database">Ukládám databázi…</string>
|
||||
<string name="space">Místo</string>
|
||||
@@ -134,7 +134,7 @@
|
||||
<string name="version_label">Verze %1$s</string>
|
||||
<string name="education_unlock_summary">Databázi odemknete zadáním hesla a/nebo souboru s klíčem.
|
||||
\n
|
||||
\nNezapomeňte si po každé úpravě zazálohovat kopii svého .kdbx souboru na bezpečné místo.</string>
|
||||
\nNezapomeňte po každé úpravě zálohovat kopii svého .kdbx souboru na bezpečné místo.</string>
|
||||
<string-array name="timeout_options">
|
||||
<item>5 sekund</item>
|
||||
<item>10 sekund</item>
|
||||
@@ -155,41 +155,41 @@
|
||||
<string name="encryption">Šifrování</string>
|
||||
<string name="key_derivation_function">Funkce pro tvorbu klíče</string>
|
||||
<string name="extended_ASCII">Rozšířené ASCII</string>
|
||||
<string name="allow">Umožnit</string>
|
||||
<string name="error_load_database">Databázi se nedaří načíst.</string>
|
||||
<string name="error_load_database_KDF_memory">Klíč se nedaří načíst, zkuste snížit množství paměti, využívané funkcí pro tvorbu klíče.</string>
|
||||
<string name="error_autofill_enable_service">Službu automatického vyplňování se nedaří zapnout.</string>
|
||||
<string name="allow">Povolit</string>
|
||||
<string name="error_load_database">Databázi se nepodařilo načíst.</string>
|
||||
<string name="error_load_database_KDF_memory">Klíč se nepodařilo načíst, zkuste snížit \"využití paměti\" pro KDF.</string>
|
||||
<string name="error_autofill_enable_service">Službu automatického vyplňování se nepodařilo zapnout.</string>
|
||||
<string name="error_move_folder_in_itself">Není možné přesunout skupinu do ní samotné.</string>
|
||||
<string name="file_not_found_content">Soubor nenalezen. Zkuste jej otevřít ze správce souborů.</string>
|
||||
<string name="list_entries_show_username_title">Zobrazit uživatelská jména</string>
|
||||
<string name="list_entries_show_username_summary">V seznamech položek zobrazit uživatelská jména</string>
|
||||
<string name="list_entries_show_username_summary">V seznamech záznamů zobrazit uživatelská jména</string>
|
||||
<string name="copy_field">Kopie %1$s</string>
|
||||
<string name="menu_form_filling_settings">Vyplňování formulářů</string>
|
||||
<string name="menu_copy">Zkopírovat</string>
|
||||
<string name="menu_move">Přesunout</string>
|
||||
<string name="menu_paste">Vložit</string>
|
||||
<string name="menu_cancel">Storno</string>
|
||||
<string name="menu_cancel">Zrušit</string>
|
||||
<string name="menu_file_selection_read_only">Chráněno před zápisem</string>
|
||||
<string name="menu_open_file_read_and_write">Čtení a zápis</string>
|
||||
<string name="read_only">Chráněno před zápisem</string>
|
||||
<string name="encryption_explanation">Algoritmus šifrování databáze užitý pro všechna data.</string>
|
||||
<string name="kdf_explanation">Klíč pro šifrovací algoritmus je vytvořen transformací hlavního klíče skrze odvozovací funkci klíče s náhodně přidanou složkou, tzv. solí.</string>
|
||||
<string name="encryption_explanation">Algoritmus šifrování databáze použit pro všechna data.</string>
|
||||
<string name="kdf_explanation">Klíč pro šifrovací algoritmus je vytvořen transformací hlavního klíče pomocí funkce odvození klíče s náhodně přidanou složkou, tzv. solí.</string>
|
||||
<string name="memory_usage">Využití paměti</string>
|
||||
<string name="memory_usage_explanation">Množství paměti (v bajtech) použitých funkcí pro odvození klíče.</string>
|
||||
<string name="parallelism">Souběžné zpracovávání</string>
|
||||
<string name="parallelism_explanation">Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro vytvoření klíče.</string>
|
||||
<string name="parallelism_explanation">Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro odvození klíče.</string>
|
||||
<string name="sort_menu">Seřadit</string>
|
||||
<string name="sort_ascending">Nejnižší první ↓</string>
|
||||
<string name="sort_groups_before">Skupiny první</string>
|
||||
<string name="sort_recycle_bin_bottom">Koš jako poslední</string>
|
||||
<string name="sort_title">Nadpis</string>
|
||||
<string name="sort_username">Uživatelské jméno</string>
|
||||
<string name="sort_creation_time">Vytvoření</string>
|
||||
<string name="sort_last_modify_time">Změna</string>
|
||||
<string name="sort_creation_time">Založeno</string>
|
||||
<string name="sort_last_modify_time">Změněno</string>
|
||||
<string name="sort_last_access_time">Přístup</string>
|
||||
<string name="warning">Varování</string>
|
||||
<string name="warning_password_encoding">Nepoužívejte v hesle pro databázový soubor znaky mimo znakovou sadu Latin-1 (nepoužívejte znaky s diakritikou).</string>
|
||||
<string name="warning_empty_password">Pokračovat bez ochrany odemknutím heslem\?</string>
|
||||
<string name="warning_password_encoding">Nepoužívejte v hesle pro databázový soubor znaky mimo znaky kódování textu (nerozpoznané znaky budou konvertovány na stejné písmeno).</string>
|
||||
<string name="warning_empty_password">Pokračovat bez ochrany odemknutí heslem\?</string>
|
||||
<string name="warning_no_encryption_key">Pokračovat bez šifrovacího klíče\?</string>
|
||||
<string name="encrypted_value_stored">Šifrované heslo uloženo</string>
|
||||
<string name="no_credentials_stored">Tato databáze zatím nemá uložené heslo.</string>
|
||||
@@ -198,7 +198,7 @@
|
||||
<string name="general">Obecné</string>
|
||||
<string name="autofill">Automatické vyplnění</string>
|
||||
<string name="autofill_service_name">KeePassDX automatické vyplňování formulářů</string>
|
||||
<string name="autofill_sign_in_prompt">Přihlásit se pomocí KeePassDX</string>
|
||||
<string name="autofill_sign_in_prompt">Přihlásit se s KeePassDX</string>
|
||||
<string name="set_autofill_service_title">Nastavit výchozí službu automatického vyplňování</string>
|
||||
<string name="autofill_explanation_summary">Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích</string>
|
||||
<string name="password_size_title">Délka generovaného hesla</string>
|
||||
@@ -206,28 +206,28 @@
|
||||
<string name="list_password_generator_options_title">Znaky hesla</string>
|
||||
<string name="list_password_generator_options_summary">Nastavit povolené znaky pro generátor hesel</string>
|
||||
<string name="clipboard">Schránka</string>
|
||||
<string name="clipboard_notifications_title">Oznamování schránky</string>
|
||||
<string name="clipboard_notifications_summary">Ukázat oznamení schránky o kopírování pole při prohlížení záznamu</string>
|
||||
<string name="clipboard_notifications_title">Oznámení schránky</string>
|
||||
<string name="clipboard_notifications_summary">Ukázat oznámení schránky o kopírování pole při prohlížení záznamu</string>
|
||||
<string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string>
|
||||
<string name="lock">Zamknout</string>
|
||||
<string name="lock_database_screen_off_title">Zámek obrazovky</string>
|
||||
<string name="lock_database_screen_off_summary">Při zhasnutí obrazovky uzamknout databázi</string>
|
||||
<string name="advanced_unlock">Rozšířené odemknutí</string>
|
||||
<string name="biometric_unlock_enable_title">Biometrické odemčení</string>
|
||||
<string name="biometric_unlock_enable_title">Biometrické odemknutí</string>
|
||||
<string name="biometric_unlock_enable_summary">Nechá otevřít databázi snímáním biometrického údaje</string>
|
||||
<string name="biometric_delete_all_key_title">Smazat šifrovací klíče</string>
|
||||
<string name="biometric_delete_all_key_summary">Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí</string>
|
||||
<string name="unavailable_feature_text">Tuto funkci se nedaří spustit.</string>
|
||||
<string name="unavailable_feature_version">V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější.</string>
|
||||
<string name="unavailable_feature_hardware">Hardware nebyl rozpoznán.</string>
|
||||
<string name="unavailable_feature_hardware">Odpovídající hardware nebyl rozpoznán.</string>
|
||||
<string name="file_name">Název souboru</string>
|
||||
<string name="path">Cesta</string>
|
||||
<string name="assign_master_key">Přiřadit hlavní klíč</string>
|
||||
<string name="create_keepass_file">Vytvořit novou databázi</string>
|
||||
<string name="create_keepass_file">Založit novou databázi</string>
|
||||
<string name="recycle_bin_title">Využití koše</string>
|
||||
<string name="recycle_bin_summary">Před smazáním přesune skupiny a položky do skupiny „Koš“</string>
|
||||
<string name="monospace_font_fields_enable_title">Písmo položek</string>
|
||||
<string name="monospace_font_fields_enable_summary">Čitelnost znaků v položkách můžete přizpůsobit změnou písma</string>
|
||||
<string name="monospace_font_fields_enable_title">Písmo kolonek</string>
|
||||
<string name="monospace_font_fields_enable_summary">Čitelnost znaků v kolonkách můžete přizpůsobit změnou písma</string>
|
||||
<string name="allow_copy_password_title">Důvěřovat schránce</string>
|
||||
<string name="allow_copy_password_summary">Povolit kopírování hesla záznamu a chráněných položek do schránky</string>
|
||||
<string name="allow_copy_password_warning">Varování: Schránka je sdílena všemi aplikacemi. Pokud jsou do ní zkopírovány citlivé údaje, mohl by se k nim dostat další software.</string>
|
||||
@@ -253,47 +253,47 @@
|
||||
<string name="education_create_database_summary">Založte svůj první soubor pro správu hesel.</string>
|
||||
<string name="education_select_database_title">Otevřít existující databázi</string>
|
||||
<string name="education_select_database_summary">Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání.</string>
|
||||
<string name="education_new_node_title">Přidejte položky do databáze</string>
|
||||
<string name="education_new_node_summary">Položky pomáhají se správou vašich digitálních identit.
|
||||
\n
|
||||
<string name="education_new_node_title">Přidejte záznamy do databáze</string>
|
||||
<string name="education_new_node_summary">Záznamy pomáhají se správou Vašich digitálních identit.
|
||||
\n
|
||||
\nSkupiny (ekvivalent složek) organizují záznamy v databázi.</string>
|
||||
<string name="education_search_title">Hledejte v položkách</string>
|
||||
<string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné položky k nalezení svých hesel.</string>
|
||||
<string name="education_entry_edit_title">Upravit položku</string>
|
||||
<string name="education_entry_edit_summary">Přidejte ke své položce vlastní kolonky. Společná data mohou být sdílena mezi více různými kolonkami.</string>
|
||||
<string name="education_search_title">Hledat v záznamech</string>
|
||||
<string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné kolonky k nalezení svých hesel.</string>
|
||||
<string name="education_entry_edit_title">Upravit záznam</string>
|
||||
<string name="education_entry_edit_summary">Přidejte ke svému záznamu vlastní kolonky. Společná data mohou být sdílena mezi různými kolonkami záznamu odkazem.</string>
|
||||
<string name="education_generate_password_title">Vytvořit silné heslo</string>
|
||||
<string name="education_generate_password_summary">Vygenerujte silné heslo pro svou položku, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo.</string>
|
||||
<string name="education_generate_password_summary">Generujte silné heslo pro svůj záznam, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo.</string>
|
||||
<string name="education_entry_new_field_title">Přidat vlastní kolonky</string>
|
||||
<string name="education_entry_new_field_summary">Registrovat další kolonku, zadat hodnotu a volitelně ji ochránit.</string>
|
||||
<string name="education_unlock_title">Odemknout databázi</string>
|
||||
<string name="education_read_only_title">Ochraňte svou databázi před zápisem</string>
|
||||
<string name="education_read_only_summary">Změnit režim otevírání pro dané sezení.
|
||||
\n
|
||||
\nV režimu \"pouze pro čtení\" zabráníte nechtěným změnám do databáze.
|
||||
\nV režimu \"pouze pro čtení\" zabráníte nechtěným změnám v databázi.
|
||||
\nV režimu \"zápisu\" je možné přidávat, mazat nebo měnit všechny prvky podle libosti.</string>
|
||||
<string name="education_field_copy_title">Zkopírujte kolonku</string>
|
||||
<string name="education_field_copy_summary">Zkopírované kolonky lze vkládat kam chcete
|
||||
\n
|
||||
<string name="education_field_copy_title">Zkopírovat kolonku</string>
|
||||
<string name="education_field_copy_summary">Zkopírované kolonky lze vkládat podle libosti.
|
||||
\n
|
||||
\nK vyplňování formulářů použijte svou oblíbenou metodu.</string>
|
||||
<string name="education_lock_title">Uzamkni databáze</string>
|
||||
<string name="education_lock_summary">Rychlé uzamkni databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky.</string>
|
||||
<string name="education_lock_title">Uzamknout databázi</string>
|
||||
<string name="education_lock_summary">Rychle uzamknout databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky.</string>
|
||||
<string name="education_sort_title">Řazení položek</string>
|
||||
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
|
||||
<string name="education_donation_title">Zapojit se</string>
|
||||
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a přidávání dalších funkcí.</string>
|
||||
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel, tato je <strong>bez reklam</strong>", je "<strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
|
||||
<string name="html_text_buy_pro">Zakoupením varianty „pro“ získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
|
||||
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, bezpečnost a doplnění dalších funkcí.</string>
|
||||
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato <strong>bez reklam</strong>, je \u0020<strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
|
||||
<string name="html_text_buy_pro">Zakoupením varianty \"pro\" získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
|
||||
<string name="html_text_feature_generosity">Tento <strong>vizuální styl</strong> je k dispozici díky vaší štědrosti.</string>
|
||||
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity, počítáme s vaším <strong>přispěním.</strong></string>
|
||||
<string name="html_text_dev_feature">Tato funkce je <strong>ve vývoji</strong> a potřebuje váš <strong>příspěvek</strong>, aby byla brzy k dispozici.</string>
|
||||
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším <strong>přispěním.</strong></string>
|
||||
<string name="html_text_dev_feature">Tato funkce je <strong>ve vývoji</strong> a potřebuje Váš <strong>příspěvek</strong>, aby byla brzy k dispozici.</string>
|
||||
<string name="html_text_dev_feature_buy_pro">Zakoupením <strong>pro</strong> varianty,</string>
|
||||
<string name="html_text_dev_feature_contibute"><strong>Zapojením se</strong>,</string>
|
||||
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k přidávání <strong>nových funkcí</strong> a <strong>opravování chyb</strong> dle vašich připomínek.</string>
|
||||
<string name="html_text_dev_feature_thanks">Mnohé díky za vaše přispění.</string>
|
||||
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k doplnění <strong>nových funkcí</strong> a <strong>opravám chyb</strong> dle vašich připomínek.</string>
|
||||
<string name="html_text_dev_feature_thanks">Mockrát děkujeme za Váš příspěvek.</string>
|
||||
<string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string>
|
||||
<string name="html_text_dev_feature_upgrade">Pamatujte na aktualizaci aplikace instalováním nových verzí.</string>
|
||||
<string name="download">Stáhnout</string>
|
||||
<string name="contribute">Zapojit se</string>
|
||||
<string name="contribute">Přispět</string>
|
||||
<string name="encryption_chacha20">ChaCha20</string>
|
||||
<string name="kdf_AES">AES</string>
|
||||
<string name="style_choose_title">Vzhled aplikace</string>
|
||||
@@ -304,16 +304,16 @@
|
||||
<string name="keyboard_name">Magikeyboard</string>
|
||||
<string name="keyboard_label">Magikeyboard (KeePassDX)</string>
|
||||
<string name="keyboard_setting_label">Magikeyboard nastavení</string>
|
||||
<string name="keyboard_entry_category">Položka</string>
|
||||
<string name="keyboard_entry_category">Záznam</string>
|
||||
<string name="keyboard_entry_timeout_title">Časový limit</string>
|
||||
<string name="keyboard_entry_timeout_summary">Doba uchování položky v Magikeyboardu</string>
|
||||
<string name="keyboard_notification_entry_title">Informace o oznámení</string>
|
||||
<string name="keyboard_notification_entry_summary">Zobrazit oznámení, když je položka dostupná</string>
|
||||
<string name="keyboard_notification_entry_content_title_text">Položka</string>
|
||||
<string name="keyboard_notification_entry_content_title_text">Záznam</string>
|
||||
<string name="keyboard_notification_entry_content_title">%1$s dostupné v Magikeyboardu</string>
|
||||
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
||||
<string name="keyboard_notification_entry_clear_close_title">Vymazat při zavření</string>
|
||||
<string name="keyboard_notification_entry_clear_close_summary">Zavři databázi při zavření oznámení</string>
|
||||
<string name="keyboard_notification_entry_clear_close_summary">Zavřít databázi při zavření oznámení</string>
|
||||
<string name="keyboard_appearance_category">Vzhled</string>
|
||||
<string name="keyboard_theme_title">Vzhled klávesnice</string>
|
||||
<string name="keyboard_keys_category">Klávesy</string>
|
||||
@@ -326,33 +326,33 @@
|
||||
<string name="clear_clipboard_notification_title">Vymazat při ukončení</string>
|
||||
<string name="clear_clipboard_notification_summary">Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení</string>
|
||||
<string name="recycle_bin">Koš</string>
|
||||
<string name="keyboard_selection_entry_title">Výběr položky</string>
|
||||
<string name="keyboard_selection_entry_summary">Při prohlížení záznamu ukázat na Magikeyboard pole položek</string>
|
||||
<string name="keyboard_selection_entry_title">Výběr záznamu</string>
|
||||
<string name="keyboard_selection_entry_summary">Při prohlížení záznamu ukázat na Magikeyboard kolonky</string>
|
||||
<string name="delete_entered_password_title">Smazat heslo</string>
|
||||
<string name="delete_entered_password_summary">Smaže heslo zadané po pokusu o připojení k databázi</string>
|
||||
<string name="content_description_open_file">Otevřít soubor</string>
|
||||
<string name="content_description_node_children">Potomci uzlu</string>
|
||||
<string name="content_description_add_node">Přidej uzel</string>
|
||||
<string name="content_description_add_entry">Přidej záznam</string>
|
||||
<string name="content_description_node_children">Podřazené prvky uzlu</string>
|
||||
<string name="content_description_add_node">Přidat uzel</string>
|
||||
<string name="content_description_add_entry">Přidat záznam</string>
|
||||
<string name="content_description_add_group">Přidat skupinu</string>
|
||||
<string name="content_description_file_information">Informace o souboru</string>
|
||||
<string name="content_description_password_checkbox">Checkbox hesla</string>
|
||||
<string name="content_description_keyfile_checkbox">Checkbox souboru s klíčem</string>
|
||||
<string name="content_description_repeat_toggle_password_visibility">Přepni ukázání hesla</string>
|
||||
<string name="content_description_repeat_toggle_password_visibility">Opakovat přepnutí viditelnosti hesla</string>
|
||||
<string name="content_description_entry_icon">Ikona záznamu</string>
|
||||
<string name="entry_password_generator">Generátor hesel</string>
|
||||
<string name="content_description_password_length">Délka hesla</string>
|
||||
<string name="entry_add_field">Přidej pole</string>
|
||||
<string name="content_description_remove_field">Odeber pole</string>
|
||||
<string name="entry_add_field">Přidat pole</string>
|
||||
<string name="content_description_remove_field">Odebrat pole</string>
|
||||
<string name="entry_UUID">UUID</string>
|
||||
<string name="error_move_entry_here">Sem záznam přesunout nelze.</string>
|
||||
<string name="error_copy_entry_here">Sem záznam zkopírovat nelze.</string>
|
||||
<string name="list_groups_show_number_entries_title">Ukaž počet záznamů</string>
|
||||
<string name="list_groups_show_number_entries_summary">Ukaž počet záznamů ve skupině</string>
|
||||
<string name="list_groups_show_number_entries_title">Ukázat počet záznamů</string>
|
||||
<string name="list_groups_show_number_entries_summary">Ukázat počet záznamů ve skupině</string>
|
||||
<string name="content_description_background">Pozadí</string>
|
||||
<string name="content_description_update_from_list">Aktualizovat</string>
|
||||
<string name="content_description_keyboard_close_fields">Zavři kolonky</string>
|
||||
<string name="error_create_database_file">Nelze vytvořit databázi s tímto heslem a klíčem ze souboru.</string>
|
||||
<string name="content_description_keyboard_close_fields">Zavřít pole</string>
|
||||
<string name="error_create_database_file">Nelze vytvořit databázi s tímto heslem a souborem klíče.</string>
|
||||
<string name="menu_advanced_unlock_settings">Rozšířené odemknutí</string>
|
||||
<string name="biometric">Biometrika</string>
|
||||
<string name="biometric_auto_open_prompt_title">Automaticky otevřít pobídku</string>
|
||||
@@ -372,7 +372,7 @@
|
||||
<string name="entry_otp">OTP</string>
|
||||
<string name="error_invalid_OTP">Neplatná OTP tajnost.</string>
|
||||
<string name="error_disallow_no_credentials">Nejméně jeden přihlašovací údaj musí být zadán.</string>
|
||||
<string name="error_copy_group_here">Sem skupinu kopírovat nemůžete.</string>
|
||||
<string name="error_copy_group_here">Sem skupinu kopírovat nelze.</string>
|
||||
<string name="error_otp_secret_key">Tajný klíč musí mít formát Base32.</string>
|
||||
<string name="error_otp_counter">Čítač musít být mezi %1$d a %2$d.</string>
|
||||
<string name="error_otp_period">Interval musít být mezi %1$d a %2$d vteřinami.</string>
|
||||
@@ -384,7 +384,7 @@
|
||||
<string name="contains_duplicate_uuid">Databáze obsahuje duplikátní UUID.</string>
|
||||
<string name="contains_duplicate_uuid_procedure">Opravit chybu založením nového UUID pro duplikáty a pokračovat\?</string>
|
||||
<string name="database_opened">Databáze otevřena</string>
|
||||
<string name="clipboard_explanation_summary">Kopírujte pole záznamů pomocí schránky Vašeho zařízení</string>
|
||||
<string name="clipboard_explanation_summary">Kopírovat kolonky záznamů pomocí schránky svého zařízení</string>
|
||||
<string name="advanced_unlock_explanation_summary">K snadnějšímu otevření databáze použijte rozšířené odemknutí</string>
|
||||
<string name="database_data_compression_title">Komprese dat</string>
|
||||
<string name="database_data_compression_summary">Komprese dat snižuje velikost databáze</string>
|
||||
@@ -413,20 +413,20 @@
|
||||
<string name="recycle_bin_group_title">Skupina Koš</string>
|
||||
<string name="enable_auto_save_database_title">Uložit databázi automaticky</string>
|
||||
<string name="enable_auto_save_database_summary">Uložit databázi po každé důležité akci (v režimu \"Zápis\")</string>
|
||||
<string name="entry_attachments">Připojené soubory</string>
|
||||
<string name="entry_attachments">Přílohy</string>
|
||||
<string name="menu_restore_entry_history">Obnovit historii</string>
|
||||
<string name="menu_delete_entry_history">Smazat historii</string>
|
||||
<string name="keyboard_auto_go_action_title">Akce auto-klávesy</string>
|
||||
<string name="keyboard_auto_go_action_summary">Akce klávesy \"Jít\" po stisknutí klávesy \"Položka\"</string>
|
||||
<string name="keyboard_auto_go_action_summary">Akce klávesy \"Jít\" po stisknutí klávesy \"Kolonka\"</string>
|
||||
<string name="download_attachment">Stáhnout %1$s</string>
|
||||
<string name="download_initialization">Zahajuji…</string>
|
||||
<string name="download_progression">Probíhá: %1$d%%</string>
|
||||
<string name="download_finalization">Dokončuji…</string>
|
||||
<string name="download_complete">Kompletní!</string>
|
||||
<string name="hide_expired_entries_title">Skrýt propadlé záznamy</string>
|
||||
<string name="hide_expired_entries_summary">Propadlé záznamy nejsou ukázány</string>
|
||||
<string name="hide_expired_entries_summary">Propadlé záznamy nebudou ukázány</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="contribution">Příspěvky</string>
|
||||
<string name="contribution">Příspění</string>
|
||||
<string name="feedback">Feedback</string>
|
||||
<string name="auto_focus_search_title">Snadné hledání</string>
|
||||
<string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string>
|
||||
@@ -436,23 +436,23 @@
|
||||
<string name="remember_keyfile_locations_summary">Uchová informaci o tom, kde jsou uloženy soubory s klíči</string>
|
||||
<string name="show_recent_files_title">Ukázat nedávné soubory</string>
|
||||
<string name="show_recent_files_summary">Ukázat umístění nedávných databází</string>
|
||||
<string name="hide_broken_locations_title">Skrýt špatné odkazy na databáze</string>
|
||||
<string name="hide_broken_locations_summary">Skrýt nesprávné odkazy v seznamu nedávných databází</string>
|
||||
<string name="hide_broken_locations_title">Skrýt chybné odkazy na databáze</string>
|
||||
<string name="hide_broken_locations_summary">Skrýt chybné odkazy v seznamu nedávných databází</string>
|
||||
<string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je <strong>open source</strong> a <strong>bey reklam</strong>.
|
||||
\nJe poskytován jak je, pod licencí <strong>GPLv3</strong>, bez jakékoli záruky.</string>
|
||||
<string name="html_about_contribution">Abychom si <strong>udrželi svoji svobodu</strong>, <strong>opravili chyby</strong>,<strong>doplnili funkce</strong> a <strong>byli vždy aktivní</strong>, počítáme s Vaším <strong>přispěním</strong>.</string>
|
||||
<string name="error_create_database">Nedaří se vytvořit soubor s databází.</string>
|
||||
<string name="error_create_database">Nepodařilo se vytvořit soubor databáze.</string>
|
||||
<string name="entry_add_attachment">Přidat přílohu</string>
|
||||
<string name="discard">Zahodit</string>
|
||||
<string name="discard_changes">Zahodit změny\?</string>
|
||||
<string name="validate">Ověřit</string>
|
||||
<string name="discard">Zavrhnout</string>
|
||||
<string name="discard_changes">Zavrhnout změny\?</string>
|
||||
<string name="validate">Zkontrolovat</string>
|
||||
<string name="education_setup_OTP_summary">Nastavit správu One-Time hesla (HOTP / TOTP) pro založení tokenu požadovaného pro dvoufázové ověření (2FA).</string>
|
||||
<string name="education_setup_OTP_title">Nastavit OTP</string>
|
||||
<string name="autofill_auto_search_summary">Automaticky navrhnout výsledky hledání z webové domény nebo ID aplikace</string>
|
||||
<string name="autofill_auto_search_title">Samočinné hledání</string>
|
||||
<string name="lock_database_show_button_summary">Ukáže tlačítko zámku v uživatelském rozhraní</string>
|
||||
<string name="lock_database_show_button_title">Ukázat tlačítko zámku</string>
|
||||
<string name="lock_database_show_button_summary">Zobrazí tlačítko zámku v uživatelském rozhraní</string>
|
||||
<string name="lock_database_show_button_title">Zobrazit tlačítko zámku</string>
|
||||
<string name="autofill_preference_title">Nastavení samovyplnění</string>
|
||||
<string name="warning_database_link_revoked">Přístup k souboru zrušenému správcem souborů</string>
|
||||
<string name="error_label_exists">Tento štítek již existuje.</string>
|
||||
@@ -469,14 +469,14 @@
|
||||
<string name="subdomain_search_title">Hledat v subdoméně</string>
|
||||
<string name="error_string_type">Tento text se s požadovanou položkou neshoduje.</string>
|
||||
<string name="content_description_add_item">Přidat položku</string>
|
||||
<string name="keyboard_previous_fill_in_summary">Automaticky přepnout na předchozí klávesnici po provedení Akce auto-klávesy</string>
|
||||
<string name="keyboard_previous_fill_in_summary">Automaticky přepnout na předchozí klávesnici po provedení \"Akce auto-klávesy\"</string>
|
||||
<string name="keyboard_previous_fill_in_title">Akce auto-klávesy</string>
|
||||
<string name="keyboard_previous_database_credentials_summary">Automaticky přepnout zpět na předchozí klávesnici na obrazovce ověřovacích údajů databáze</string>
|
||||
<string name="keyboard_previous_database_credentials_title">Obrazovka ověřovacích údajů databáze</string>
|
||||
<string name="keyboard_change">Přepnout klávesnici</string>
|
||||
<string name="upload_attachment">Nahrát %1$s</string>
|
||||
<string name="content_description_credentials_information">Info o údajích</string>
|
||||
<string name="warning_file_too_big">Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory).
|
||||
<string name="content_description_credentials_information">Informace o údajích</string>
|
||||
<string name="warning_file_too_big">Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory s klíči).
|
||||
\n
|
||||
\nVaše databáze by se mohla značně zvětšit a tímto nahráním tak ztratit na výkonnosti.</string>
|
||||
<string name="warning_replace_file">Nahráním tohoto souboru nahradíte existující soubor.</string>
|
||||
@@ -497,7 +497,7 @@
|
||||
<string name="autofill_ask_to_save_data_title">Zeptat se před uložením</string>
|
||||
<string name="autofill_save_search_info_summary">Pokuste se uložit údaje hledání, když manuálně vybíráte položku</string>
|
||||
<string name="autofill_save_search_info_title">Uložit info hledání</string>
|
||||
<string name="autofill_close_database_summary">Zavřít databázi po samodoplnění polí</string>
|
||||
<string name="autofill_close_database_summary">Zavřít databázi po samovyplnění polí</string>
|
||||
<string name="autofill_close_database_title">Zavřít databázi</string>
|
||||
<string name="keyboard_previous_lock_summary">Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici</string>
|
||||
<string name="keyboard_previous_lock_title">Uzamknout databázi</string>
|
||||
@@ -506,17 +506,17 @@
|
||||
<string name="notification">Oznámení</string>
|
||||
<string name="biometric_security_update_required">Vyžadována aktualizace biometrického zabezpečení.</string>
|
||||
<string name="configure_biometric">Žádné přihlašovací ani biometrické údaje nejsou registrovány.</string>
|
||||
<string name="warning_empty_recycle_bin">Trvale odstranit všechny položky z koše\?</string>
|
||||
<string name="warning_empty_recycle_bin">Trvale odstranit všechny uzly z koše\?</string>
|
||||
<string name="registration_mode">Režim registrace</string>
|
||||
<string name="save_mode">Režim ukládání</string>
|
||||
<string name="search_mode">Režim vyhledávání</string>
|
||||
<string name="error_field_name_already_exists">Jméno položky již existuje.</string>
|
||||
<string name="error_field_name_already_exists">Jméno kolonky již existuje.</string>
|
||||
<string name="error_registration_read_only">Uložení nové položky v režimu databáze pouze pro čtení není povoleno</string>
|
||||
<string name="enter">Enter</string>
|
||||
<string name="backspace">Backspace</string>
|
||||
<string name="select_entry">Vybrat záznam</string>
|
||||
<string name="back_to_previous_keyboard">Zpět na předchozí klávesnici</string>
|
||||
<string name="custom_fields">Vlastní položky</string>
|
||||
<string name="custom_fields">Vlastní kolonky</string>
|
||||
<string name="advanced_unlock_delete_all_key_warning">Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí\?</string>
|
||||
<string name="device_credential_unlock_enable_summary">Dovolí pro otevření databáze použít heslo Vašeho zařízení</string>
|
||||
<string name="device_credential_unlock_enable_title">Odemknutí heslem zařízení</string>
|
||||
@@ -534,7 +534,7 @@
|
||||
<string name="open_advanced_unlock_prompt_unlock_database">Pro odemknutí databáze otevřete pobídku rozšířeného odemknutí</string>
|
||||
<string name="menu_keystore_remove_key">Smazat klíč rozšířeného odemknutí</string>
|
||||
<string name="education_advanced_unlock_title">Rozšířené odemknutí databáze</string>
|
||||
<string name="advanced_unlock_timeout">Timeout rozšířeného odemknutí</string>
|
||||
<string name="advanced_unlock_timeout">Časový limit rozšířeného odemknutí</string>
|
||||
<string name="temp_advanced_unlock_timeout_summary">Trvání použití rozšířeného odemknutí než bude obsah téhož smazán</string>
|
||||
<string name="temp_advanced_unlock_enable_summary">Za účelem rozšířeného odemknutí neukládat žádný šifrovaný obsah</string>
|
||||
<string name="temp_advanced_unlock_enable_title">Přechodné rozšířené odemknutí</string>
|
||||
@@ -544,4 +544,6 @@
|
||||
<string name="education_advanced_unlock_summary">Abyste rychle odemknuli databázi, propojte své heslo s naskenovanou biometrikou nebo údaji zámku zařízení.</string>
|
||||
<string name="temp_advanced_unlock_timeout_title">Vypršení pokročilého odemknutí</string>
|
||||
<string name="content">Obsah</string>
|
||||
<string name="error_rebuild_list">Seznam nelze řádně sestavit.</string>
|
||||
<string name="error_database_uri_null">URI databáze nelze načíst.</string>
|
||||
</resources>
|
||||
@@ -555,4 +555,6 @@
|
||||
<string name="advanced_unlock_prompt_extract_credential_message">Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
</resources>
|
||||
<string name="error_rebuild_list">Die Liste kann nicht ordnungsgemäß neu erstellt werden.</string>
|
||||
<string name="error_database_uri_null">Datenbank-URI kann nicht abgerufen werden.</string>
|
||||
</resources>
|
||||
@@ -543,4 +543,6 @@
|
||||
<string name="content">Περιεχόμενα</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">Δεν είναι δυνατή η σωστή αναδημιουργία της λίστας.</string>
|
||||
<string name="error_database_uri_null">Δεν είναι δυνατή η ανάκτηση του URI βάσης δεδομένων.</string>
|
||||
</resources>
|
||||
@@ -275,7 +275,7 @@
|
||||
<string name="contribute">Contribuir</string>
|
||||
<string name="encryption_chacha20">ChaCha20</string>
|
||||
<string name="kdf_AES">AES</string>
|
||||
<string name="style_choose_title">Tema de aplicación</string>
|
||||
<string name="style_choose_title">Tema de la aplicación</string>
|
||||
<string name="style_choose_summary">Tema utilizado en la aplicación</string>
|
||||
<string name="icon_pack_choose_title">Seleccione un paquete de iconos</string>
|
||||
<string name="icon_pack_choose_summary">Cambiar el paquete de iconos en la aplicación</string>
|
||||
@@ -545,4 +545,6 @@
|
||||
<string name="keyboard_search_share_summary">Buscar automáticamente la información compartida para llenar el teclado</string>
|
||||
<string name="show_uuid_summary">Muestra el UUID vinculado a una entrada</string>
|
||||
<string name="show_uuid_title">Mostrar UUID</string>
|
||||
<string name="error_rebuild_list">No es posible reconstruir adecuadamente la lista.</string>
|
||||
<string name="error_database_uri_null">La URI de la base de datos no puede ser recuperada.</string>
|
||||
</resources>
|
||||
@@ -552,4 +552,6 @@
|
||||
<string name="content">Contenu</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">Impossible de reconstruire correctement la liste.</string>
|
||||
<string name="error_database_uri_null">L\'URI de la base de données ne peut pas être récupéré.</string>
|
||||
</resources>
|
||||
@@ -527,4 +527,6 @@
|
||||
<string name="temp_advanced_unlock_enable_title">Privremeno napredno otključavanje</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">Nije moguće ispravno obnoviti popis.</string>
|
||||
<string name="error_database_uri_null">URI baze podataka nije moguće dobiti.</string>
|
||||
</resources>
|
||||
@@ -404,7 +404,7 @@
|
||||
<string name="remember_keyfile_locations_title">Ricorda posizione file chiave</string>
|
||||
<string name="remember_database_locations_summary">Ricorda la posizione dei database</string>
|
||||
<string name="remember_database_locations_title">Ricorda posizione database</string>
|
||||
<string name="contains_duplicate_uuid_procedure">Per continuare, risolvi il problema generando nuovi UUID per i duplicati\?</string>
|
||||
<string name="contains_duplicate_uuid_procedure">Risolvi il problema generando nuovi UUID per i duplicati per continuare\?</string>
|
||||
<string name="error_create_database">Impossibile creare il file del database.</string>
|
||||
<string name="entry_add_attachment">Aggiungi allegato</string>
|
||||
<string name="discard">Scarta</string>
|
||||
@@ -428,7 +428,7 @@
|
||||
<string name="settings_database_recommend_changing_master_key_title">Rinnovo raccomandato</string>
|
||||
<string name="hide_expired_entries_summary">Le voci scadute non sono mostrate</string>
|
||||
<string name="hide_expired_entries_title">Nascondi le voci scadute</string>
|
||||
<string name="education_setup_OTP_summary">Imposta la gestione delle OTP (HOTP / TOTP) per generare un token richiesto per la 2FA.</string>
|
||||
<string name="education_setup_OTP_summary">Imposta la gestione della password usa e getta (HOTP/TOTP) per generare un token richiesto per l\'autenticazione a due fattori.</string>
|
||||
<string name="download_complete">Completato!</string>
|
||||
<string name="download_finalization">Finalizzazione…</string>
|
||||
<string name="download_progression">Avanzamento %1$d%%</string>
|
||||
@@ -451,7 +451,7 @@
|
||||
<string name="enable">Abilita</string>
|
||||
<string name="settings_database_force_changing_master_key_next_time_summary">Richiedi il cambio della chiave principale la prossima volta (una volta)</string>
|
||||
<string name="settings_database_force_changing_master_key_next_time_title">Forza il rinnovo la prossima volta</string>
|
||||
<string name="settings_database_force_changing_master_key_summary">Richiedi il cambio della master key (giorni)</string>
|
||||
<string name="settings_database_force_changing_master_key_summary">Richiedi la modifica della chiave principale (giorni)</string>
|
||||
<string name="lock_database_show_button_summary">Mostra il bottone di blocco nell\'interfaccia utente</string>
|
||||
<string name="lock_database_show_button_title">Mostra il bottone di blocco</string>
|
||||
<string name="autofill_preference_title">Impostazioni dell\'autocompletamento</string>
|
||||
@@ -546,4 +546,6 @@
|
||||
<string name="menu_keystore_remove_key">Elimina chiave di sblocco avanzato</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">Non è possibile ricostruire la lista correttamente.</string>
|
||||
<string name="error_database_uri_null">Non è stato recuperato l\'indirizzo del database.</string>
|
||||
</resources>
|
||||
@@ -541,4 +541,7 @@
|
||||
<string name="temp_advanced_unlock_enable_title">一時的な高度なロック解除</string>
|
||||
<string name="advanced_unlock_tap_delete">タップして高度なロック解除用の鍵を削除する</string>
|
||||
<string name="content">コンテンツ</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_database_uri_null">データベースのURIが見つかりません。</string>
|
||||
</resources>
|
||||
@@ -529,11 +529,16 @@
|
||||
<string name="menu_keystore_remove_key">Usuń zaawansowany klucz odblokowujący</string>
|
||||
<string name="education_advanced_unlock_summary">Połącz swoje hasło ze zeskanowanymi danymi biometrycznymi lub danymi logowania urządzenia, aby szybko odblokować bazę danych.</string>
|
||||
<string name="education_advanced_unlock_title">Zaawansowane odblokowywanie bazy danych</string>
|
||||
<string name="advanced_unlock_timeout">Zaawansowany limit czasu odblokowania</string>
|
||||
<string name="advanced_unlock_timeout">Limit czasu zaawansowanego odblokowywania</string>
|
||||
<string name="temp_advanced_unlock_timeout_summary">Czas trwania zaawansowanego odblokowywania przed usunięciem jego zawartości</string>
|
||||
<string name="temp_advanced_unlock_timeout_title">Wygaśnięcie zaawansowanego odblokowania</string>
|
||||
<string name="temp_advanced_unlock_enable_summary">Nie przechowuj żadnych zaszyfrowanych treści, aby korzystać z zaawansowanego odblokowywania</string>
|
||||
<string name="advanced_unlock_tap_delete">Naciśnij, aby usunąć zaawansowane klucze odblokowujące</string>
|
||||
<string name="content">Zawartość</string>
|
||||
<string name="advanced_unlock_prompt_extract_credential_title">Otwórz bazę danych z zaawansowanym rozpoznawaniem odblokowania</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="advanced_unlock_scanning_error">Błąd zaawansowanego odblokowywania: %1$s</string>
|
||||
<string name="error_rebuild_list">Nie można poprawnie odbudować listy.</string>
|
||||
<string name="error_database_uri_null">Nie można pobrać identyfikatora URI bazy danych.</string>
|
||||
</resources>
|
||||
@@ -543,4 +543,6 @@
|
||||
<string name="content">Содержимое</string>
|
||||
<string name="kdf_Argon2id">Argon2ID</string>
|
||||
<string name="kdf_Argon2d">Argon2D</string>
|
||||
<string name="error_database_uri_null">Невозможно получить URI базы.</string>
|
||||
<string name="error_rebuild_list">Невозможно правильно перестроить список.</string>
|
||||
</resources>
|
||||
@@ -527,4 +527,6 @@
|
||||
<string name="content">İçerik</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">Liste düzgün şekilde yeniden oluşturulamıyor.</string>
|
||||
<string name="error_database_uri_null">Veri tabanı URI\'si alınamıyor.</string>
|
||||
</resources>
|
||||
@@ -543,4 +543,6 @@
|
||||
<string name="content">Вміст</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">Не вдалося належним чином відновити список.</string>
|
||||
<string name="error_database_uri_null">Неможливо отримати URI бази даних.</string>
|
||||
</resources>
|
||||
22
app/src/main/res/values-v30/donottranslate.xml
Normal file
22
app/src/main/res/values-v30/donottranslate.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<resources>
|
||||
<bool name="autofill_inline_suggestions_default" translatable="false">true</bool>
|
||||
</resources>
|
||||
@@ -543,4 +543,6 @@
|
||||
<string name="content">内容</string>
|
||||
<string name="kdf_Argon2id">Argon2id</string>
|
||||
<string name="kdf_Argon2d">Argon2d</string>
|
||||
<string name="error_rebuild_list">无法正确地重建列表。</string>
|
||||
<string name="error_database_uri_null">无法检索数据库 URI 。</string>
|
||||
</resources>
|
||||
@@ -151,6 +151,8 @@
|
||||
<bool name="autofill_close_database_default" translatable="false">false</bool>
|
||||
<string name="autofill_auto_search_key" translatable="false">autofill_auto_search_key</string>
|
||||
<bool name="autofill_auto_search_default" translatable="false">true</bool>
|
||||
<string name="autofill_inline_suggestions_key" translatable="false">autofill_inline_suggestions_key</string>
|
||||
<bool name="autofill_inline_suggestions_default" translatable="false">false</bool>
|
||||
<string name="autofill_save_search_info_key" translatable="false">autofill_save_search_info_key</string>
|
||||
<bool name="autofill_save_search_info_default" translatable="false">true</bool>
|
||||
<string name="autofill_ask_to_save_data_key" translatable="false">autofill_ask_to_save_data_key</string>
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
<string name="menu_hide_password">Hide password</string>
|
||||
<string name="menu_lock">Lock database</string>
|
||||
<string name="menu_save_database">Save database</string>
|
||||
<string name="menu_reload_database">Reload database</string>
|
||||
<string name="menu_open">Open</string>
|
||||
<string name="menu_search">Search</string>
|
||||
<string name="menu_showpass">Show password</string>
|
||||
@@ -272,6 +273,9 @@
|
||||
<string name="warning_sure_remove_data">Remove this data anyway?</string>
|
||||
<string name="warning_empty_keyfile">It is not recommended to add an empty keyfile.</string>
|
||||
<string name="warning_empty_keyfile_explanation">The content of the keyfile should never be changed, and in the best case, should contain randomly generated data.</string>
|
||||
<string name="warning_database_info_changed">The information contained in your database file has been modified outside the app.</string>
|
||||
<string name="warning_database_info_changed_options">Overwrite the external modifications by saving the database or reload it with the latest changes.</string>
|
||||
<string name="warning_database_revoked">Access to the file revoked by the file manager, close the database and reopen it from its location.</string>
|
||||
<string name="version_label">Version %1$s</string>
|
||||
<string name="build_label">Build %1$s</string>
|
||||
<string name="configure_biometric">No biometric or device credential is enrolled.</string>
|
||||
@@ -428,6 +432,8 @@
|
||||
<string name="autofill_close_database_summary">Close the database after an autofill selection</string>
|
||||
<string name="autofill_auto_search_title">Auto search</string>
|
||||
<string name="autofill_auto_search_summary">Automatically suggest search results from the web domain or application ID</string>
|
||||
<string name="autofill_inline_suggestions_title">Inline suggestions</string>
|
||||
<string name="autofill_inline_suggestions_summary">Attempt to display autofill suggestions directly from a compatible keyboard</string>
|
||||
<string name="autofill_save_search_info_title">Save search info</string>
|
||||
<string name="autofill_save_search_info_summary">Try to save search information when making a manual entry selection</string>
|
||||
<string name="autofill_ask_to_save_data_title">Ask to save data</string>
|
||||
@@ -439,6 +445,7 @@
|
||||
<string name="autofill_block">Block autofill</string>
|
||||
<string name="autofill_block_restart">Restart the app containing the form to activate the blocking.</string>
|
||||
<string name="autofill_read_only_save">Data save is not allowed for a database opened as read-only.</string>
|
||||
<string name="autofill_inline_suggestions_keyboard">Autofill suggestions added.</string>
|
||||
<string name="allow_no_password_title">Allow no master key</string>
|
||||
<string name="allow_no_password_summary">Allows tapping the \"Open\" button if no credentials are selected</string>
|
||||
<string name="delete_entered_password_title">Delete password</string>
|
||||
|
||||
@@ -465,6 +465,7 @@
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
@@ -23,7 +23,9 @@ Settings Activity. This is pointed to in the service's meta-data in the applicat
|
||||
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:targetApi="p"
|
||||
android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity" >
|
||||
android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
|
||||
android:supportsInlineSuggestions="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<compatibility-package
|
||||
android:name="com.amazon.cloud9"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
android:title="@string/autofill_auto_search_title"
|
||||
android:summary="@string/autofill_auto_search_summary"
|
||||
android:defaultValue="@bool/autofill_auto_search_default"/>
|
||||
<SwitchPreference
|
||||
android:key="@string/autofill_inline_suggestions_key"
|
||||
android:title="@string/autofill_inline_suggestions_title"
|
||||
android:summary="@string/autofill_inline_suggestions_summary"
|
||||
android:defaultValue="@bool/autofill_inline_suggestions_default"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/save">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.4.10'
|
||||
ext.kotlin_version = '1.4.21'
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/52.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/52.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
* Fix specific attachments with kdbx3.1 databases #828
|
||||
* Fix small bugs
|
||||
8
fastlane/metadata/android/en-US/changelogs/53.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/53.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
* 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
|
||||
2
fastlane/metadata/android/fr-FR/changelogs/52.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/52.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
* Correction des pièces jointes spécifiques avec les bases kdbx3.1 #828
|
||||
* Correction de petits bugs
|
||||
8
fastlane/metadata/android/fr-FR/changelogs/53.txt
Normal file
8
fastlane/metadata/android/fr-FR/changelogs/53.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
* Détection des changements de fichiers et rechargement de base de données #794
|
||||
* Remplissage automatique avec suggestions en ligne (Android R) (Android R) #827
|
||||
* Ajout du fichier de clé XML version 2 #844
|
||||
* Correction des binaires de 64 Octets #835
|
||||
* Recherche spéciale dans les champs de recherche #830
|
||||
* Priorité au bouton OTP dans les notifications #845
|
||||
* Correction de génération OTP pour longue clé secrère #848
|
||||
* Correction de petits bugs #849
|
||||
@@ -1,7 +1,7 @@
|
||||
<b>Multi-format KeePass password manager</b>, the app allows saving and using passwords, keys and digital identities in a secure way, by integrating the Android design standards.
|
||||
|
||||
This pro version is under development, buying it encourages <b>faster development</b>, <b>better service</b>, and you contribute to the creation of <b>open source softwares without advertising</b>.
|
||||
<i>Currently, the application has the same features as the free version with the themes unlocked but is intended to integrate the elements of connection and synchronization facilitated for sites and services commonly used.</i>
|
||||
<i>Currently, the application has the same features as the free version with the themes unlocked but is intended to integrate elements related to non-free sites and services commonly used.</i>
|
||||
|
||||
<b>Features</b>
|
||||
- Create database files / entries and groups.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<b>Gestionnaire de mots de passe KeePass multiformats</b>, l'application permet d'enregistrer et d'utiliser des mots de passe, des clés et des identités numériques de manière sécurisée, en intégrant les normes de conception Android.
|
||||
|
||||
Cette version pro est en cours de développement, en l'achetant vous encouragez <b>un développement plus rapide</b>, <b>un meilleur service</b> et vous contribuez à la création de <b>logiciels open source sans publicité</b>.
|
||||
<i>Actuellement, l'application possède les mêmes fonctionnalités que la version gratuite avec les thèmes débloqués mais est destinée à intégrer les éléments de connexion et de synchronisation facilités pour les sites et services couramment utilisés.</i>
|
||||
<i>Actuellement, l'application possède les mêmes fonctionnalités que la version gratuite avec les thèmes débloqués mais est destinée à intégrer des éléments liés à des sites et services non gratuits couramment utilisés.</i>
|
||||
|
||||
<b>Fonctionnalités</b>
|
||||
- Création de bases de données / entrées et groupes.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<b>複数の形式に対応する KeePass パスワード マネージャー</b>。Android の設計基準が組み込まれており、パスワード、鍵、デジタル ID を安全な方法で保存して使用できます。
|
||||
|
||||
この pro バージョンは開発中です。購入することで<b>開発の加速</b>と<b>サービスの改善</b>を支援し、<b>広告なしのオープンソース ソフトウェア</b>の作成に貢献できます。
|
||||
<i>現在、このアプリケーションの機能はテーマのロックが解除された free バージョンと同じです。よく利用されるサイトやサービスの接続と同期を楽にする要素を統合することが予定されています。</i>
|
||||
<i>現在、このアプリケーションの機能はテーマのロックが解除された free バージョンと同じです。一般的に使われている不自由なサイトやサービスに関連する要素を統合することを計画しています。</i>
|
||||
|
||||
<b>機能</b>
|
||||
- データベースファイル / エントリー・グループの作成
|
||||
|
||||
Reference in New Issue
Block a user