diff --git a/CHANGELOG b/CHANGELOG index 3ab9bac28..e131d31d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,61 @@ +KeePassDX(2.9.13) + * Fix TOTP plugin settings #878 + * Allow Emoji #796 + * Scroll and better UI in entry edition screen #876 + +KeePassDX(2.9.12) + * Fix OTP token type #863 + * Fix auto open biometric prompt #862 + * Fix back appearance setting #865 + * Fix orientation change in settings #872 + * Change memory unit to MiB #851 + * Small changes #642 + +KeePassDX(2.9.11) + * Add Keyfile XML version 2 (fix hex) #844 + * Fix hex Keyfile #861 + +KeePassDX(2.9.10) + * Try to fix autofill #852 + * Fix database change dialog displayed too often #853 + +KeePassDX(2.9.9) + * Detect file changes and reload database #794 + * Inline suggestions autofill with compatible keyboard (Android R) #827 + * Add Keyfile XML version 2 #844 + * Fix binaries of 64 bytes #835 + * Special search in title fields #830 + * Priority to OTP button in notifications #845 + * Fix OTP generation for long secret key #848 + * Fix small bugs #849 + +KeePassDX(2.9.8) + * Fix specific attachments with kdbx3.1 databases #828 + * Fix small bugs + +KeePassDX(2.9.7) + * Remove write permission since Android 10 #823 + * Fix small bugs + +KeePassDX(2.9.6) + * Fix KeyFile bug #820 + +KeePassDX(2.9.5) + * Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811 + * Prevent auto switch back to previous keyboard if otp field exists #814 + * Fix timeout reset #817 + +KeePassDX(2.9.4) + * Fix small bugs #812 + * Argon2ID implementation #791 + KeePassDX(2.9.3) * Unlock database by device credentials (PIN/Password/Pattern) #779 #102 + * Advanced unlock with timeout #102 #437 #566 + * Remove default database parameter when the file is no longer accessible #803 + * Move OTP button to the first view level in Magikeyboard #587 + * Tooltips for Magikeyboard #586 + * Fix small bugs #805 KeePassDX(2.9.2) * Managing OTP links from QR applications #556 diff --git a/app/build.gradle b/app/build.gradle index 513ecacf3..1020de134 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,22 +5,22 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 30 - buildToolsVersion '30.0.2' + buildToolsVersion '30.0.3' ndkVersion '21.3.6528147' defaultConfig { applicationId "com.kunzisoft.keepass" minSdkVersion 14 targetSdkVersion 30 - versionCode = 47 - versionName = "2.9.3" + versionCode = 57 + versionName = "2.9.13" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" testInstrumentationRunner = "android.test.InstrumentationTestRunner" buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}" - manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ] + manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ] kapt { arguments { @@ -92,7 +92,7 @@ android { } } -def room_version = "2.2.5" +def room_version = "2.2.6" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" @@ -110,6 +110,8 @@ dependencies { // Database implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" + // Autofill + implementation "androidx.autofill:autofill:1.1.0-rc01" // Crypto implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' // Time @@ -121,7 +123,7 @@ dependencies { // Apache Commons Collections implementation 'commons-collections:commons-collections:3.2.2' // Apache Commons Codec - implementation 'commons-codec:commons-codec:1.14' + implementation 'commons-codec:commons-codec:1.15' // Icon pack implementation project(path: ':icon-pack-classic') implementation project(path: ':icon-pack-material') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fece32d8c..7bcdcb9a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,10 +14,15 @@ android:name="android.permission.USE_BIOMETRIC" /> + + android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="28" + tools:ignore="ScopedStorage" /> + + android:name="android.permission.QUERY_ALL_PACKAGES" + tools:ignore="QueryAllPackagesPermission" /> + android:windowSoftInputMode="adjustResize" /> + @@ -207,7 +216,7 @@ diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt index bfa581810..dd1433aed 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt @@ -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 } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index 8849d26fe..97523d19c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -39,7 +39,9 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.appbar.CollapsingToolbarLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper +import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry @@ -49,20 +51,19 @@ import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.StreamDirection -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService -import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY +import com.kunzisoft.keepass.otp.OtpEntryFields +import com.kunzisoft.keepass.services.AttachmentFileNotificationService +import com.kunzisoft.keepass.services.ClipboardEntryNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.MenuUtil -import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.createDocument -import com.kunzisoft.keepass.utils.onCreateDocumentResult +import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.EntryContentsView -import com.kunzisoft.keepass.view.showActionError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded import java.util.* import kotlin.collections.HashMap @@ -133,7 +134,7 @@ class EntryActivity : LockingActivity() { } // Focus view to reinitialize timeout - resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout) + coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this) // Init the clipboard helper clipboardHelper = ClipboardHelper(this) @@ -150,8 +151,13 @@ class EntryActivity : LockingActivity() { if (result.isSuccess) finish() } + ACTION_DATABASE_RELOAD_TASK -> { + // Close the current activity + this.showActionErrorIfNeeded(result) + finish() + } } - coordinatorLayout?.showActionError(result) + coordinatorLayout?.showActionErrorIfNeeded(result) } } @@ -198,8 +204,7 @@ class EntryActivity : LockingActivity() { // Refresh Menu invalidateOptionsMenu() - val entryInfo = entry.getEntryInfo(Database.getInstance()) - + val entryInfo = entry.getEntryInfo(mDatabase) // Manage entry copy to start notification if allowed if (mFirstLaunchOfActivity) { // Manage entry to launch copying notification if allowed @@ -231,23 +236,21 @@ class EntryActivity : LockingActivity() { private fun fillEntryDataInContentsView(entry: Entry) { - val database = Database.getInstance() - database.startManageEntry(entry) + val entryInfo = entry.getEntryInfo(mDatabase) + // Assign title icon - titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor) + titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor) // Assign title text - val entryTitle = entry.title + val entryTitle = entryInfo.title collapsingToolbarLayout?.title = entryTitle toolbar?.title = entryTitle // Assign basic fields - entryContentsView?.assignUserName(entry.username) { - database.startManageEntry(entry) - clipboardHelper?.timeoutCopyToClipboard(entry.username, + entryContentsView?.assignUserName(entryInfo.username) { + clipboardHelper?.timeoutCopyToClipboard(entryInfo.username, getString(R.string.copy_field, getString(R.string.entry_user_name))) - database.stopManageEntry(entry) } val isFirstTimeAskAllowCopyPasswordAndProtectedFields = @@ -277,11 +280,9 @@ class EntryActivity : LockingActivity() { val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) { View.OnClickListener { - database.startManageEntry(entry) - clipboardHelper?.timeoutCopyToClipboard(entry.password, + clipboardHelper?.timeoutCopyToClipboard(entryInfo.password, getString(R.string.copy_field, getString(R.string.entry_password))) - database.stopManageEntry(entry) } } else { // If dialog not already shown @@ -291,44 +292,46 @@ class EntryActivity : LockingActivity() { null } } - entryContentsView?.assignPassword(entry.password, + entryContentsView?.assignPassword(entryInfo.password, allowCopyPasswordAndProtectedFields, onPasswordCopyClickListener) //Assign OTP field - entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress, - View.OnClickListener { - entry.getOtpElement()?.let { otpElement -> - clipboardHelper?.timeoutCopyToClipboard( - otpElement.token, - getString(R.string.copy_field, getString(R.string.entry_otp)) - ) - } - }) + entry.getOtpElement()?.let { otpElement -> + entryContentsView?.assignOtp(otpElement, entryProgress) { + clipboardHelper?.timeoutCopyToClipboard( + otpElement.token, + getString(R.string.copy_field, getString(R.string.entry_otp)) + ) + } + } - entryContentsView?.assignURL(entry.url) - entryContentsView?.assignNotes(entry.notes) + entryContentsView?.assignURL(entryInfo.url) + entryContentsView?.assignNotes(entryInfo.notes) // Assign custom fields if (mDatabase?.allowEntryCustomFields() == true) { entryContentsView?.clearExtraFields() - entry.getExtraFields().forEach { field -> + entryInfo.customFields.forEach { field -> val label = field.name - val value = field.protectedValue - val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields - if (allowCopyProtectedField) { - entryContentsView?.addExtraField(label, value, allowCopyProtectedField) { - clipboardHelper?.timeoutCopyToClipboard( - value.toString(), - getString(R.string.copy_field, label) - ) - } - } else { - // If dialog not already shown - if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) { - entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener) + // OTP field is already managed in dedicated view + if (label != OtpEntryFields.OTP_TOKEN_FIELD) { + val value = field.protectedValue + val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields + if (allowCopyProtectedField) { + entryContentsView?.addExtraField(label, value, allowCopyProtectedField) { + clipboardHelper?.timeoutCopyToClipboard( + value.toString(), + getString(R.string.copy_field, label) + ) + } } else { - entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null) + // If dialog not already shown + if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) { + entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener) + } else { + entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null) + } } } } @@ -336,24 +339,16 @@ class EntryActivity : LockingActivity() { entryContentsView?.setHiddenProtectedValue(!mShowPassword) // Manage attachments - mDatabase?.binaryPool?.let { binaryPool -> - entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem -> - createDocument(this, attachmentItem.name)?.let { requestCode -> - mAttachmentsToDownload[requestCode] = attachmentItem - } + entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem -> + createDocument(this, attachmentItem.name)?.let { requestCode -> + mAttachmentsToDownload[requestCode] = attachmentItem } } // Assign dates - entryContentsView?.assignCreationDate(entry.creationTime) - entryContentsView?.assignModificationDate(entry.lastModificationTime) - entryContentsView?.assignLastAccessDate(entry.lastAccessTime) - entryContentsView?.setExpires(entry.isCurrentlyExpires) - if (entry.expires) { - entryContentsView?.assignExpiresDate(entry.expiryTime) - } else { - entryContentsView?.assignExpiresDate(getString(R.string.never)) - } + entryContentsView?.assignCreationDate(entryInfo.creationTime) + entryContentsView?.assignModificationDate(entryInfo.modificationTime) + entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime) // Manage history historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE @@ -368,8 +363,6 @@ class EntryActivity : LockingActivity() { // Assign special data entryContentsView?.assignUUID(entry.nodeId.id) - - database.stopManageEntry(entry) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -407,6 +400,9 @@ class EntryActivity : LockingActivity() { menu.findItem(R.id.menu_save_database)?.isVisible = false menu.findItem(R.id.menu_edit)?.isVisible = false } + if (mSpecialMode != SpecialMode.DEFAULT) { + menu.findItem(R.id.menu_reload_database)?.isVisible = false + } val gotoUrl = menu.findItem(R.id.menu_goto_url) gotoUrl?.apply { @@ -500,6 +496,9 @@ class EntryActivity : LockingActivity() { R.id.menu_save_database -> { mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) } + R.id.menu_reload_database -> { + mProgressDatabaseTaskProvider?.startDatabaseReload(false) + } android.R.id.home -> finish() // close this activity and return to preview activity (if there is any) } return super.onOptionsItemSelected(item) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 698b69c4d..7542d2566 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.app.DatePickerDialog import android.app.TimePickerDialog -import android.app.assist.AssistStructure import android.content.Context import android.content.Intent import android.net.Uri @@ -37,7 +36,6 @@ import android.widget.DatePicker import android.widget.TimePicker import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView @@ -48,6 +46,8 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.icon.IconImage @@ -56,21 +56,22 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.model.* -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService -import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpEntryFields +import com.kunzisoft.keepass.services.AttachmentFileNotificationService +import com.kunzisoft.keepass.services.ClipboardEntryNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.KeyboardEntryNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil +import com.kunzisoft.keepass.view.ToolbarAction import com.kunzisoft.keepass.view.asError -import com.kunzisoft.keepass.view.showActionError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.updateLockPaddingLeft import org.joda.time.DateTime import java.util.* @@ -97,7 +98,7 @@ class EntryEditActivity : LockingActivity(), private var coordinatorLayout: CoordinatorLayout? = null private var scrollView: NestedScrollView? = null private var entryEditFragment: EntryEditFragment? = null - private var entryEditAddToolBar: Toolbar? = null + private var entryEditAddToolBar: ToolbarAction? = null private var validateButton: View? = null private var lockView: View? = null @@ -117,11 +118,12 @@ class EntryEditActivity : LockingActivity(), super.onCreate(savedInstanceState) setContentView(R.layout.activity_entry_edit) - val toolbar = findViewById(R.id.toolbar) - setSupportActionBar(toolbar) - supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp) + // Bottom Bar + entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar) + setSupportActionBar(entryEditAddToolBar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(false) coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout) @@ -134,7 +136,7 @@ class EntryEditActivity : LockingActivity(), } // Focus view to reinitialize timeout - resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout) + coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this) stopService(Intent(this, ClipboardEntryNotificationService::class.java)) stopService(Intent(this, KeyboardEntryNotificationService::class.java)) @@ -234,51 +236,6 @@ class EntryEditActivity : LockingActivity(), mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments } - // Assign title - title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry) - - // Bottom Bar - entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar) - entryEditAddToolBar?.apply { - menuInflater.inflate(R.menu.entry_edit, menu) - - menu.findItem(R.id.menu_add_field).apply { - val allowCustomField = mDatabase?.allowEntryCustomFields() == true - isEnabled = allowCustomField - isVisible = allowCustomField - } - - // Attachment not compatible below KitKat - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - menu.findItem(R.id.menu_add_attachment).isVisible = false - } - - menu.findItem(R.id.menu_add_otp).apply { - val allowOTP = mDatabase?.allowOTP == true - isEnabled = allowOTP - // OTP not compatible below KitKat - isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT - } - - setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.menu_add_field -> { - addNewCustomField() - true - } - R.id.menu_add_attachment -> { - addNewAttachment(item) - true - } - R.id.menu_add_otp -> { - setupOTP() - true - } - else -> true - } - } - } - // To retrieve attachment mSelectFileHelper = SelectFileHelper(this) mAttachmentFileBinderManager = AttachmentFileBinderManager(this) @@ -334,8 +291,13 @@ class EntryEditActivity : LockingActivity(), Log.e(TAG, "Unable to retrieve entry after database action", e) } } + ACTION_DATABASE_RELOAD_TASK -> { + // Close the current activity + this.showActionErrorIfNeeded(result) + finish() + } } - coordinatorLayout?.showActionError(result) + coordinatorLayout?.showActionErrorIfNeeded(result) } } @@ -360,7 +322,7 @@ class EntryEditActivity : LockingActivity(), // Build Autofill response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mDatabase?.let { database -> - AutofillHelper.buildResponse(this@EntryEditActivity, + AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, entry.getEntryInfo(database)) } } @@ -478,8 +440,12 @@ class EntryEditActivity : LockingActivity(), } override fun onEditCustomFieldApproved(oldField: Field, newField: Field) { - verifyNameField(newField) { + if (oldField.name.equals(newField.name, true)) { entryEditFragment?.replaceExtraField(oldField, newField) + } else { + verifyNameField(newField) { + entryEditFragment?.replaceExtraField(oldField, newField) + } } } @@ -609,18 +575,30 @@ class EntryEditActivity : LockingActivity(), override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) - - val inflater = menuInflater - inflater.inflate(R.menu.database, menu) - // Save database not needed here - menu.findItem(R.id.menu_save_database)?.isVisible = false - MenuUtil.contributionMenuInflater(inflater, menu) - + menuInflater.inflate(R.menu.entry_edit, menu) return true } - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + + menu?.findItem(R.id.menu_add_field)?.apply { + val allowCustomField = mDatabase?.allowEntryCustomFields() == true + isEnabled = allowCustomField + isVisible = allowCustomField + } + + // Attachment not compatible below KitKat + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + menu?.findItem(R.id.menu_add_attachment)?.isVisible = false + } + + menu?.findItem(R.id.menu_add_otp)?.apply { + val allowOTP = mDatabase?.allowOTP == true + isEnabled = allowOTP + // OTP not compatible below KitKat + isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + } + entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post { performedNextEducation(it) } } @@ -672,11 +650,16 @@ class EntryEditActivity : LockingActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_save_database -> { - mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) + R.id.menu_add_field -> { + addNewCustomField() + return true } - R.id.menu_contribute -> { - MenuUtil.onContributionItemSelected(this) + R.id.menu_add_attachment -> { + addNewAttachment(item) + return true + } + R.id.menu_add_otp -> { + setupOTP() return true } android.R.id.home -> { @@ -908,7 +891,7 @@ class EntryEditActivity : LockingActivity(), */ @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, group: Group, searchInfo: SearchInfo? = null) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { @@ -916,7 +899,7 @@ class EntryEditActivity : LockingActivity(), intent.putExtra(KEY_PARENT, group.nodeId) AutofillHelper.startActivityForAutofillResult(activity, intent, - assistStructure, + autofillComponent, searchInfo) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt index 2c5560487..acd3c5c37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditFragment.kt @@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment +import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.database.element.Attachment @@ -148,6 +149,8 @@ class EntryEditFragment: StylishFragment() { iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE taIconColor?.recycle() + rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext()) + // Retrieve the new entry after an orientation change if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true) mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 60058a243..e2c4672c5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity -import android.app.assist.AssistStructure import android.content.Context import android.content.Intent import android.net.Uri @@ -36,7 +35,6 @@ import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator @@ -49,16 +47,18 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.asError @@ -162,7 +162,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } // Observe list of databases - databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles -> + databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles -> when (databaseFiles.databaseFileAction) { DatabaseFilesViewModel.DatabaseFileAction.NONE -> { mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList) @@ -186,13 +186,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } } databaseFilesViewModel.consumeAction() - }) + } // Observe default database - databaseFilesViewModel.defaultDatabase.observe(this, Observer { + databaseFilesViewModel.defaultDatabase.observe(this) { // Retrieve settings for default database mAdapterDatabaseHistory?.setDefaultDatabase(it) - }) + } // Attach the dialog thread to this activity mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply { @@ -200,8 +200,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), when (actionTask) { ACTION_DATABASE_CREATE_TASK -> { result.data?.getParcelable(DATABASE_URI_KEY)?.let { databaseUri -> - val keyFileUri = result.data?.getParcelable(KEY_FILE_URI_KEY) - databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri) + val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential() + databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri) } } ACTION_DATABASE_LOAD_TASK -> { @@ -237,10 +237,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), private fun fileNoFoundAction(e: FileNotFoundException) { val error = getString(R.string.file_not_found_content) + Log.e(TAG, error, e) coordinatorLayout?.let { Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show() } - Log.e(TAG, error, e) } private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { @@ -331,9 +331,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri) } - override fun onAssignKeyDialogPositiveClick( - masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) { + override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { try { mDatabaseFileUri?.let { databaseUri -> @@ -341,10 +339,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), // Create the new database mProgressDatabaseTaskProvider?.startDatabaseCreate( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } } catch (e: Exception) { @@ -354,11 +349,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } } - override fun onAssignKeyDialogNegativeClick( - masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) { - - } + override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -435,8 +426,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), when (item.itemId) { android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url) } - - return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item) + MenuUtil.onDefaultMenuOptionsItemSelected(this, item) + return super.onOptionsItemSelected(item) } companion object { @@ -502,11 +493,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null) { AutofillHelper.startActivityForAutofillResult(activity, Intent(activity, FileDatabaseSelectActivity::class.java), - assistStructure, + autofillComponent, searchInfo) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index b8b1b57cf..48bb26c59 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.app.SearchManager -import android.app.assist.AssistStructure import android.content.ComponentName import android.content.Context import android.content.Intent @@ -50,7 +49,9 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry @@ -65,15 +66,16 @@ import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil @@ -153,7 +155,7 @@ class GroupActivity : LockingActivity(), taTextColor.recycle() // Focus view to reinitialize timeout - resetAppTimeoutWhenViewFocusedOrChanged(rootContainerView) + rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this) // Retrieve elements after an orientation change if (savedInstanceState != null) { @@ -227,10 +229,10 @@ class GroupActivity : LockingActivity(), currentGroup, searchInfo) onLaunchActivitySpecialMode() }, - { searchInfo, assistStructure -> + { searchInfo, autofillComponent -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { EntryEditActivity.launchForAutofillResult(this@GroupActivity, - assistStructure, + autofillComponent, currentGroup, searchInfo) onLaunchActivitySpecialMode() } else { @@ -316,7 +318,12 @@ class GroupActivity : LockingActivity(), if (result.isSuccess) { // Rebuild all the list to avoid bug when delete node from sort - mListNodesFragment?.rebuildList() + try { + mListNodesFragment?.rebuildList() + } catch (e: Exception) { + Log.e(TAG, "Unable to rebuild the list after deletion") + e.printStackTrace() + } // Add trash in views list if it doesn't exists if (database.isRecycleBinEnabled) { @@ -336,9 +343,20 @@ class GroupActivity : LockingActivity(), } } } + ACTION_DATABASE_RELOAD_TASK -> { + // Reload the current activity + if (result.isSuccess) { + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } else { + this.showActionErrorIfNeeded(result) + finish() + } + } } - coordinatorLayout?.showActionError(result) + coordinatorLayout?.showActionErrorIfNeeded(result) finishNodeAction() @@ -659,7 +677,7 @@ class GroupActivity : LockingActivity(), // Build response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { mDatabase?.let { database -> - AutofillHelper.buildResponse(this, + AutofillHelper.buildResponseAndSetResult(this, entry.getEntryInfo(database)) } } @@ -872,6 +890,8 @@ class GroupActivity : LockingActivity(), } if (mSpecialMode == SpecialMode.DEFAULT) { MenuUtil.defaultMenuInflater(inflater, menu) + } else { + menu.findItem(R.id.menu_reload_database)?.isVisible = false } // Menu for recycle bin @@ -997,6 +1017,10 @@ class GroupActivity : LockingActivity(), mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) return true } + R.id.menu_reload_database -> { + mProgressDatabaseTaskProvider?.startDatabaseReload(false) + return true + } R.id.menu_empty_recycle_bin -> { mCurrentGroup?.getChildren()?.let { listChildren -> // Automatically delete all elements @@ -1124,7 +1148,16 @@ class GroupActivity : LockingActivity(), private fun rebuildListNodes() { mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment? // to refresh fragment - mListNodesFragment?.rebuildList() + try { + mListNodesFragment?.rebuildList() + } catch (e: Exception) { + e.printStackTrace() + coordinatorLayout?.let { coordinatorLayout -> + Snackbar.make(coordinatorLayout, + R.string.error_rebuild_list, + Snackbar.LENGTH_LONG).asError().show() + } + } mCurrentGroup = mListNodesFragment?.mainGroup // Remove search in intent deletePreviousSearchGroup() @@ -1295,14 +1328,14 @@ class GroupActivity : LockingActivity(), @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, readOnly: Boolean, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null, autoSearch: Boolean = false) { checkTimeAndBuildIntent(activity, null, readOnly) { intent -> intent.putExtra(AUTO_SEARCH_KEY, autoSearch) AutofillHelper.startActivityForAutofillResult(activity, intent, - assistStructure, + autofillComponent, searchInfo) } } @@ -1419,21 +1452,21 @@ class GroupActivity : LockingActivity(), } ) }, - { searchInfo, assistStructure -> + { searchInfo, autofillComponent -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { SearchHelper.checkAutoSearchInfo(activity, Database.getInstance(), searchInfo, { items -> // Response is build - AutofillHelper.buildResponse(activity, items) + AutofillHelper.buildResponseAndSetResult(activity, items) onValidateSpecialMode() }, { // Here no search info found, disable auto search GroupActivity.launchForAutofillResult(activity, readOnly, - assistStructure, + autofillComponent, searchInfo, false) onLaunchActivitySpecialMode() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt index e5657f5cc..63deba4ef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesFragment.kt @@ -22,30 +22,24 @@ package com.kunzisoft.keepass.activities import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log +import android.view.* +import androidx.appcompat.view.ActionMode import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import android.util.Log -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.view.ActionMode - import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.adapters.NodeAdapter -import com.kunzisoft.keepass.database.element.SortNodeEnum -import com.kunzisoft.keepass.database.element.Group -import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper -import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode +import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.SortNodeEnum +import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.settings.PreferencesUtil import java.util.* class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { @@ -197,7 +191,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } // Refresh data - rebuildList() + try { + rebuildList() + } catch (e: Exception) { + Log.e(TAG, "Unable to rebuild the list during resume") + e.printStackTrace() + } if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) { // To show the " no search entry found " @@ -209,10 +208,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } } + @Throws(IllegalArgumentException::class) fun rebuildList() { // Add elements to the list mainGroup?.let { mainGroup -> mAdapter?.apply { + // Thrown an exception when sort cannot be performed rebuildList(mainGroup) // To visually change the elements if (PreferencesUtil.APPEARANCE_CHANGED) { @@ -231,8 +232,13 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } // Tell the adapter to refresh it's list - mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters) - rebuildList() + try { + mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters) + rebuildList() + } catch (e:Exception) { + Log.e(TAG, "Unable to rebuild the list with the sort") + e.printStackTrace() + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index e31f58523..502a06581 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity -import android.app.assist.AssistStructure import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -37,9 +36,8 @@ import android.widget.* import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar -import androidx.biometric.BiometricManager import androidx.core.app.ActivityCompat -import androidx.lifecycle.Observer +import androidx.fragment.app.commit import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog @@ -50,33 +48,33 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager -import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper +import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException +import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import kotlinx.android.synthetic.main.activity_password.* import java.io.FileNotFoundException -open class PasswordActivity : SpecialModeActivity() { +open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener { // Views private var toolbar: Toolbar? = null @@ -86,12 +84,12 @@ open class PasswordActivity : SpecialModeActivity() { private var confirmButtonView: Button? = null private var checkboxPasswordView: CompoundButton? = null private var checkboxKeyFileView: CompoundButton? = null - private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null + private var advancedUnlockFragment: AdvancedUnlockFragment? = null private var infoContainerView: ViewGroup? = null - private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null private val databaseFileViewModel: DatabaseFileViewModel by viewModels() + private var mDefaultDatabase: Boolean = false private var mDatabaseFileUri: Uri? = null private var mDatabaseKeyFileUri: Uri? = null @@ -113,7 +111,6 @@ open class PasswordActivity : SpecialModeActivity() { private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null - private var advancedUnlockedManager: AdvancedUnlockedManager? = null private var mAllowAutoOpenBiometricPrompt: Boolean = true override fun onCreate(savedInstanceState: Bundle?) { @@ -133,7 +130,6 @@ open class PasswordActivity : SpecialModeActivity() { keyFileSelectionView = findViewById(R.id.keyfile_selection) checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxKeyFileView = findViewById(R.id.keyfile_checkox) - advancedUnlockInfoView = findViewById(R.id.biometric_info) infoContainerView = findViewById(R.id.activity_password_info_container) mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked @@ -160,10 +156,6 @@ open class PasswordActivity : SpecialModeActivity() { } }) - enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ -> - enableOrNotTheConfirmationButton() - } - // If is a view intent getUriFromIntent(intent) if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) { @@ -173,8 +165,31 @@ open class PasswordActivity : SpecialModeActivity() { mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) } + // Init Biometric elements + advancedUnlockFragment = supportFragmentManager + .findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment? + if (advancedUnlockFragment == null) { + advancedUnlockFragment = AdvancedUnlockFragment() + supportFragmentManager.commit { + replace(R.id.fragment_advanced_unlock_container_view, + advancedUnlockFragment!!, + UNLOCK_FRAGMENT_TAG) + } + } + + // Listen password checkbox to init advanced unlock and confirmation button + checkboxPasswordView?.setOnCheckedChangeListener { _, _ -> + advancedUnlockFragment?.checkUnlockAvailability() + enableOrNotTheConfirmationButton() + } + + // Observe if default database + databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> + mDefaultDatabase = isDefaultDatabase + } + // Observe database file change - databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile -> + databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile -> // Force read only if the file does not exists mForceReadOnly = databaseFile?.let { !it.databaseFileExists @@ -194,19 +209,14 @@ open class PasswordActivity : SpecialModeActivity() { filenameView?.text = databaseFile?.databaseAlias ?: "" onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) - }) + } mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply { onActionFinish = { actionTask, result -> when (actionTask) { ACTION_DATABASE_LOAD_TASK -> { // Recheck advanced unlock if error - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (PreferencesUtil.isAdvancedUnlockEnable(this@PasswordActivity)) { - // Stay with the same mode and init it - advancedUnlockedManager?.initAdvancedUnlockMode() - } - } + advancedUnlockFragment?.initAdvancedUnlockMode() if (result.isSuccess) { mDatabaseKeyFileUri = null @@ -220,32 +230,37 @@ open class PasswordActivity : SpecialModeActivity() { if (resultException != null) { resultError = resultException.getLocalizedMessage(resources) - // Relaunch loading if we need to fix UUID - if (resultException is DuplicateUuidDatabaseException) { - showLoadDatabaseDuplicateUuidMessage { + when (resultException) { + is DuplicateUuidDatabaseException -> { + // Relaunch loading if we need to fix UUID + showLoadDatabaseDuplicateUuidMessage { - var databaseUri: Uri? = null - var masterPassword: String? = null - var keyFileUri: Uri? = null - var readOnly = true - var cipherEntity: CipherDatabaseEntity? = null + var databaseUri: Uri? = null + var mainCredential: MainCredential = MainCredential() + var readOnly = true + var cipherEntity: CipherDatabaseEntity? = null - result.data?.let { resultData -> - databaseUri = resultData.getParcelable(DATABASE_URI_KEY) - masterPassword = resultData.getString(MASTER_PASSWORD_KEY) - keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY) - readOnly = resultData.getBoolean(READ_ONLY_KEY) - cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY) + result.data?.let { resultData -> + databaseUri = resultData.getParcelable(DATABASE_URI_KEY) + mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential + readOnly = resultData.getBoolean(READ_ONLY_KEY) + cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY) + } + + databaseUri?.let { databaseFileUri -> + showProgressDialogAndLoadDatabase( + databaseFileUri, + mainCredential, + readOnly, + cipherEntity, + true) + } } - - databaseUri?.let { databaseFileUri -> - showProgressDialogAndLoadDatabase( - databaseFileUri, - masterPassword, - keyFileUri, - readOnly, - cipherEntity, - true) + } + is FileNotFoundDatabaseException -> { + // Remove this default database inaccessible + if (mDefaultDatabase) { + databaseFileViewModel.removeDefaultDatabase() } } } @@ -277,6 +292,9 @@ open class PasswordActivity : SpecialModeActivity() { mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE) } + mDatabaseFileUri?.let { + databaseFileViewModel.checkIfIsDefaultDatabase(it) + } } override fun onNewIntent(intent: Intent?) { @@ -303,6 +321,33 @@ open class PasswordActivity : SpecialModeActivity() { finish() } + override fun retrieveCredentialForEncryption(): String { + return passwordView?.text?.toString() ?: "" + } + + override fun conditionToStoreCredential(): Boolean { + return checkboxPasswordView?.isChecked == true + } + + override fun onCredentialEncrypted(databaseUri: Uri, + encryptedCredential: String, + ivSpec: String) { + // Load the database if password is registered with biometric + verifyCheckboxesAndLoadDatabase( + CipherDatabaseEntity( + databaseUri.toString(), + encryptedCredential, + ivSpec) + ) + } + + override fun onCredentialDecrypted(databaseUri: Uri, + decryptedCredential: String) { + // Load the database if password is retrieve from biometric + // Retrieve from biometric + verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential) + } + private val onEditorActionListener = object : TextView.OnEditorActionListener { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { if (actionId == IME_ACTION_DONE) { @@ -369,48 +414,9 @@ open class PasswordActivity : SpecialModeActivity() { verifyCheckboxesAndLoadDatabase(password, keyFileUri) } else { // Init Biometric elements - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (PreferencesUtil.isAdvancedUnlockEnable(this)) { - if (advancedUnlockedManager == null - && databaseFileUri != null) { - advancedUnlockedManager = AdvancedUnlockedManager(this, - databaseFileUri, - advancedUnlockInfoView, - checkboxPasswordView, - enableButtonOnCheckedChangeListener, - passwordView, - { passwordEncrypted, ivSpec -> - // Load the database if password is registered with biometric - if (passwordEncrypted != null && ivSpec != null) { - verifyCheckboxesAndLoadDatabase( - CipherDatabaseEntity( - databaseFileUri.toString(), - passwordEncrypted, - ivSpec) - ) - } - }, - { passwordDecrypted -> - // Load the database if password is retrieve from biometric - passwordDecrypted?.let { - // Retrieve from biometric - verifyKeyFileCheckboxAndLoadDatabase(it) - } - }) - } - advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = - mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true - advancedUnlockedManager?.checkBiometricAvailability() - } else { - advancedUnlockInfoView?.visibility = View.GONE - advancedUnlockedManager?.destroy() - advancedUnlockedManager = null - } - } - if (advancedUnlockedManager == null) { - checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) - } - checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener) + advancedUnlockFragment?.loadDatabase(databaseFileUri, + mAllowAutoOpenBiometricPrompt + && mProgressDatabaseTaskProvider?.isBinded() != true) } enableOrNotTheConfirmationButton() @@ -462,11 +468,6 @@ open class PasswordActivity : SpecialModeActivity() { override fun onPause() { mProgressDatabaseTaskProvider?.unregisterProgressTask() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - advancedUnlockedManager?.destroy() - advancedUnlockedManager = null - } - // Reinit locking activity UI variable LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null mAllowAutoOpenBiometricPrompt = true @@ -530,8 +531,7 @@ open class PasswordActivity : SpecialModeActivity() { // Show the progress dialog and load the database showProgressDialogAndLoadDatabase( databaseUri, - password, - keyFileUri, + MainCredential(password, keyFileUri), readOnly, cipherDatabaseEntity, false) @@ -540,15 +540,13 @@ open class PasswordActivity : SpecialModeActivity() { } private fun showProgressDialogAndLoadDatabase(databaseUri: Uri, - password: String?, - keyFile: Uri?, + mainCredential: MainCredential, readOnly: Boolean, cipherDatabaseEntity: CipherDatabaseEntity?, fixDuplicateUUID: Boolean) { mProgressDatabaseTaskProvider?.startDatabaseLoad( databaseUri, - password, - keyFile, + mainCredential, readOnly, cipherDatabaseEntity, fixDuplicateUUID @@ -575,11 +573,6 @@ open class PasswordActivity : SpecialModeActivity() { MenuUtil.defaultMenuInflater(inflater, menu) } - if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // biometric menu - advancedUnlockedManager?.inflateOptionsMenu(inflater, menu) - } - super.onCreateOptionsMenu(menu) launchEducation(menu) @@ -589,13 +582,13 @@ open class PasswordActivity : SpecialModeActivity() { // Check permission private fun checkPermission() { - val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE - val permissions = arrayOf(writePermission) - if (Build.VERSION.SDK_INT >= 23 + if (Build.VERSION.SDK_INT in 23..28 && !readOnly && !mPermissionAsked) { mPermissionAsked = true // Check self permission to show or not the dialog + val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE + val permissions = arrayOf(writePermission) if (toolbar != null && ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST) @@ -655,21 +648,14 @@ open class PasswordActivity : SpecialModeActivity() { performedNextEducation(passwordActivityEducation, menu) }) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && !readOnlyEducationPerformed) { - val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this) - PreferencesUtil.isAdvancedUnlockEnable(applicationContext) - && (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) - && advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE - && advancedUnlockInfoView?.unlockIconImageView != null - && passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!, - { - performedNextEducation(passwordActivityEducation, menu) - }, - { - performedNextEducation(passwordActivityEducation, menu) - }) - } + advancedUnlockFragment?.performEducation(passwordActivityEducation, + readOnlyEducationPerformed, + { + performedNextEducation(passwordActivityEducation, menu) + }, + { + performedNextEducation(passwordActivityEducation, menu) + }) } } @@ -691,10 +677,7 @@ open class PasswordActivity : SpecialModeActivity() { readOnly = !readOnly changeOpenFileReadIcon(item) } - R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - advancedUnlockedManager?.deleteEntryKey() - } - else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) + else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item) } return super.onOptionsItemSelected(item) @@ -708,6 +691,9 @@ open class PasswordActivity : SpecialModeActivity() { mAllowAutoOpenBiometricPrompt = false + // To get device credential unlock result + advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data) + // To get entry in result if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) @@ -728,7 +714,7 @@ open class PasswordActivity : SpecialModeActivity() { when (resultCode) { LockingActivity.RESULT_EXIT_LOCK -> { clearCredentialsViews() - Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) + Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) } Activity.RESULT_CANCELED -> { clearCredentialsViews() @@ -741,6 +727,8 @@ open class PasswordActivity : SpecialModeActivity() { private val TAG = PasswordActivity::class.java.name + private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG" + private const val KEY_FILENAME = "fileName" private const val KEY_KEYFILE = "keyFile" private const val VIEW_INTENT = "android.intent.action.VIEW" @@ -844,13 +832,13 @@ open class PasswordActivity : SpecialModeActivity() { fun launchForAutofillResult(activity: Activity, databaseFile: Uri, keyFile: Uri?, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo?) { buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> AutofillHelper.startActivityForAutofillResult( activity, intent, - assistStructure, + autofillComponent, searchInfo) } } @@ -908,11 +896,11 @@ open class PasswordActivity : SpecialModeActivity() { searchInfo) onLaunchActivitySpecialMode() }, - { searchInfo, assistStructure -> // Autofill Selection Action + { searchInfo, autofillComponent -> // Autofill Selection Action if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PasswordActivity.launchForAutofillResult(activity, databaseUri, keyFile, - assistStructure, + autofillComponent, searchInfo) onLaunchActivitySpecialMode() } else { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index cabface80..55a7fed9b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -37,6 +37,7 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.SelectFileHelper +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView @@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } interface AssignPasswordDialogListener { - fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) - fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?, - keyFileChecked: Boolean, keyFile: Uri?) + fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) + fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) } override fun onAttach(activity: Context) { @@ -161,17 +160,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() { } } if (!error) { - mListener?.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) dismiss() } } val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE) negativeButton.setOnClickListener { - mListener?.onAssignKeyDialogNegativeClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential()) dismiss() } } @@ -183,6 +178,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() { return super.onCreateDialog(savedInstanceState) } + private inline fun retrieveMainCredential(): MainCredential { + val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null + val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null + return MainCredential(masterPassword, keyFile) + } + override fun onResume() { super.onResume() @@ -242,9 +243,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { builder.setMessage(R.string.warning_empty_password) .setPositiveButton(android.R.string.ok) { _, _ -> if (!verifyKeyFile()) { - mListener?.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) this@AssignMasterKeyDialogFragment.dismiss() } } @@ -259,9 +258,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(it) builder.setMessage(R.string.warning_no_encryption_key) .setPositiveButton(android.R.string.ok) { _, _ -> - mListener?.onAssignKeyDialogPositiveClick( - passwordCheckBox!!.isChecked, mMasterPassword, - keyFileCheckBox!!.isChecked, mKeyFile) + mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) this@AssignMasterKeyDialogFragment.dismiss() } .setNegativeButton(android.R.string.cancel) { _, _ -> } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt new file mode 100644 index 000000000..6a83f0df7 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt @@ -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 . + * + */ +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 + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt index d2cf2a1a5..b278bf815 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt @@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.node.Node -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle open class DeleteNodesDialogFragment : DialogFragment() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt index bd5f945d3..542e8dcb9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EmptyRecycleBinDialogFragment.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.node.Node -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt index 60da08681..e9f033e2d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt @@ -26,6 +26,7 @@ import android.net.Uri import android.os.Bundle import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.model.MainCredential class PasswordEncodingDialogFragment : DialogFragment() { @@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY) - val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false - val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY) - val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false - val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY) + val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential() activity?.let { activity -> val builder = AlertDialog.Builder(activity) @@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() { builder.setPositiveButton(android.R.string.ok) { _, _ -> mListener?.onPasswordEncodingValidateListener( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } @@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() { interface Listener { fun onPasswordEncodingValidateListener(databaseUri: Uri?, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) + mainCredential: MainCredential) } companion object { private const val DATABASE_URI_KEY = "DATABASE_URI_KEY" - private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" - private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY" - private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY" - private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY" + private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL" fun getInstance(databaseUri: Uri, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?): SortDialogFragment { + mainCredential: MainCredential): SortDialogFragment { val fragment = SortDialogFragment() fragment.arguments = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) - putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) - putString(MASTER_PASSWORD_KEY, masterPassword) - putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked) - putParcelable(KEY_FILE_URI_KEY, keyFile) + putParcelable(MAIN_CREDENTIAL, mainCredential) } return fragment } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt index ef50393fc..487cd52e1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt @@ -29,10 +29,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.Spinner +import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout @@ -57,6 +54,7 @@ class SetOTPDialogFragment : DialogFragment() { private var mOtpElement: OtpElement = OtpElement() + private var otpTypeMessage: TextView? = null private var otpTypeSpinner: Spinner? = null private var otpTokenTypeSpinner: Spinner? = null private var otpSecretContainer: TextInputLayout? = null @@ -74,6 +72,8 @@ class SetOTPDialogFragment : DialogFragment() { private var totpTokenTypeAdapter: ArrayAdapter? = null private var hotpTokenTypeAdapter: ArrayAdapter? = null private var otpAlgorithmAdapter: ArrayAdapter? = null + private var mHotpTokenTypeArray: Array? = null + private var mTotpTokenTypeArray: Array? = null private var mManualEvent = false private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus -> @@ -134,6 +134,7 @@ class SetOTPDialogFragment : DialogFragment() { activity?.let { activity -> val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup? + otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message) otpTypeSpinner = root?.findViewById(R.id.setup_otp_type) otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type) otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label) @@ -183,23 +184,23 @@ class SetOTPDialogFragment : DialogFragment() { // HOTP / TOTP Type selection val otpTypeArray = OtpType.values() - otpTypeAdapter = ArrayAdapter(activity, + otpTypeAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, otpTypeArray).apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } otpTypeSpinner?.adapter = otpTypeAdapter // Otp Token type selection - val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues() + mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues() hotpTokenTypeAdapter = ArrayAdapter(activity, - android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply { + android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } // Proprietary only on closed and full version - val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues( + mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues( BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION) totpTokenTypeAdapter = ArrayAdapter(activity, - android.R.layout.simple_spinner_item, totpTokenTypeArray).apply { + android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } otpTokenTypeAdapter = hotpTokenTypeAdapter @@ -207,7 +208,7 @@ class SetOTPDialogFragment : DialogFragment() { // OTP Algorithm val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values() - otpAlgorithmAdapter = ArrayAdapter(activity, + otpAlgorithmAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, otpAlgorithmArray).apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } @@ -372,24 +373,40 @@ class SetOTPDialogFragment : DialogFragment() { } private fun upgradeTokenType() { + val tokenType = mOtpElement.tokenType when (mOtpElement.type) { OtpType.HOTP -> { otpPeriodContainer?.visibility = View.GONE otpCounterContainer?.visibility = View.VISIBLE otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter - otpTokenTypeSpinner?.setSelection(OtpTokenType - .getHotpTokenTypeValues().indexOf(mOtpElement.tokenType)) + mHotpTokenTypeArray?.let { otpTokenTypeArray -> + defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226) + } } OtpType.TOTP -> { otpPeriodContainer?.visibility = View.VISIBLE otpCounterContainer?.visibility = View.GONE otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter - otpTokenTypeSpinner?.setSelection(OtpTokenType - .getTotpTokenTypeValues().indexOf(mOtpElement.tokenType)) + mTotpTokenTypeArray?.let { otpTokenTypeArray -> + defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238) + } } } } + private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array, + tokenType: OtpTokenType, + defaultTokenType: OtpTokenType) { + val formTokenType = if (otpTokenTypeArray.contains(tokenType)) { + otpTypeMessage?.visibility = View.GONE + tokenType + } else { + otpTypeMessage?.visibility = View.VISIBLE + defaultTokenType + } + otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType)) + } + private fun upgradeParameters() { otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values() .indexOf(mOtpElement.algorithm)) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt index 23d9b3d51..c9ef7e7e7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt @@ -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 -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt index d7f15bdcd..1747edbbe 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.activities.lock import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MotionEvent @@ -59,6 +60,9 @@ abstract class LockingActivity : SpecialModeActivity() { private set override fun onCreate(savedInstanceState: Bundle?) { + + mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this) + super.onCreate(savedInstanceState) if (savedInstanceState != null @@ -83,8 +87,6 @@ abstract class LockingActivity : SpecialModeActivity() { } mExitLock = false - - mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -163,35 +165,6 @@ abstract class LockingActivity : SpecialModeActivity() { sendBroadcast(Intent(LOCK_ACTION)) } - /** - * To reset the app timeout when a view is focused or changed - */ - @SuppressLint("ClickableViewAccessibility") - protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) { - views.forEach { - it?.setOnTouchListener { _, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - // Log.d(TAG, "View touched, try to reset app timeout") - TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) - } - } - false - } - it?.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - // Log.d(TAG, "View focused, try to reset app timeout") - TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) - } - } - if (it is ViewGroup) { - for (i in 0..it.childCount) { - resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i)) - } - } - } - } - override fun onBackPressed() { if (mTimeoutEnable) { TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { @@ -204,7 +177,7 @@ abstract class LockingActivity : SpecialModeActivity() { companion object { - private const val TAG = "LockingActivity" + const val TAG = "LockingActivity" const val RESULT_EXIT_LOCK = 1450 @@ -215,3 +188,28 @@ abstract class LockingActivity : SpecialModeActivity() { var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null } } + +/** + * To reset the app timeout when a view is focused or changed + */ +@SuppressLint("ClickableViewAccessibility") +fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) { + setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + //Log.d(LockingActivity.TAG, "View touched, try to reset app timeout") + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context) + } + } + false + } + setOnFocusChangeListener { _, _ -> + //Log.d(LockingActivity.TAG, "View focused, try to reset app timeout") + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context) + } + if (this is ViewGroup) { + for (i in 0..childCount) { + getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt index 91c8e5b2f..bc9ce9365 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt @@ -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 diff --git a/app/src/main/java/com/kunzisoft/keepass/app/App.kt b/app/src/main/java/com/kunzisoft/keepass/app/App.kt index 8094648b6..5207b1e51 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/App.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/App.kt @@ -34,7 +34,7 @@ class App : MultiDexApplication() { } override fun onTerminate() { - Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) + Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) super.onTerminate() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt index 055b536d0..eec183cda 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseAction.kt @@ -19,27 +19,96 @@ */ package com.kunzisoft.keepass.app.database +import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.net.Uri +import android.os.IBinder +import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService +import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.SingletonHolderParameter +import java.util.* -class CipherDatabaseAction(applicationContext: Context) { +class CipherDatabaseAction(context: Context) { + private val applicationContext = context.applicationContext private val cipherDatabaseDao = AppDatabase .getDatabase(applicationContext) .cipherDatabaseDao() + // Temp DAO to easily remove content if object no longer in memory + private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext) + + private val mIntentAdvancedUnlockService = Intent(applicationContext, + AdvancedUnlockNotificationService::class.java) + private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null + private var mServiceConnection: ServiceConnection? = null + + private var mDatabaseListeners = LinkedList() + + fun reloadPreferences() { + useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext) + } + + @Synchronized + private fun attachService(performedAction: () -> Unit) { + // Check if a service is currently running else do nothing + if (mBinder != null) { + performedAction.invoke() + } else if (mServiceConnection == null) { + mServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { + mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder) + performedAction.invoke() + } + + override fun onServiceDisconnected(name: ComponentName?) { + mBinder = null + mServiceConnection = null + mDatabaseListeners.forEach { + it.onDatabaseCleared() + } + } + } + applicationContext.bindService(mIntentAdvancedUnlockService, + mServiceConnection!!, + Context.BIND_ABOVE_CLIENT) + if (mBinder == null) { + applicationContext.startService(mIntentAdvancedUnlockService) + } + } + } + + fun registerDatabaseListener(listener: DatabaseListener) { + mDatabaseListeners.add(listener) + } + + fun unregisterDatabaseListener(listener: DatabaseListener) { + mDatabaseListeners.remove(listener) + } + + interface DatabaseListener { + fun onDatabaseCleared() + } + fun getCipherDatabase(databaseUri: Uri, cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) { - IOActionTask( - { - cipherDatabaseDao.getByDatabaseUri(databaseUri.toString()) - }, - { - cipherDatabaseResultListener.invoke(it) - } - ).execute() + if (useTempDao) { + attachService { + cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri)) + } + } else { + IOActionTask( + { + cipherDatabaseDao.getByDatabaseUri(databaseUri.toString()) + }, + { + cipherDatabaseResultListener.invoke(it) + } + ).execute() + } } fun containsCipherDatabase(databaseUri: Uri, @@ -51,36 +120,52 @@ class CipherDatabaseAction(applicationContext: Context) { fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity, cipherDatabaseResultListener: (() -> Unit)? = null) { - IOActionTask( - { - val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) - - // Update values if element not yet in the database - if (cipherDatabaseRetrieve == null) { - cipherDatabaseDao.add(cipherDatabaseEntity) - } else { - cipherDatabaseDao.update(cipherDatabaseEntity) + if (useTempDao) { + attachService { + mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity) + cipherDatabaseResultListener?.invoke() + } + } else { + IOActionTask( + { + val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri) + // Update values if element not yet in the database + if (cipherDatabaseRetrieve == null) { + cipherDatabaseDao.add(cipherDatabaseEntity) + } else { + cipherDatabaseDao.update(cipherDatabaseEntity) + } + }, + { + cipherDatabaseResultListener?.invoke() } - }, - { - cipherDatabaseResultListener?.invoke() - } - ).execute() + ).execute() + } } fun deleteByDatabaseUri(databaseUri: Uri, cipherDatabaseResultListener: (() -> Unit)? = null) { - IOActionTask( - { - cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString()) - }, - { - cipherDatabaseResultListener?.invoke() - } - ).execute() + if (useTempDao) { + attachService { + mBinder?.deleteByDatabaseUri(databaseUri) + cipherDatabaseResultListener?.invoke() + } + } else { + IOActionTask( + { + cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString()) + }, + { + cipherDatabaseResultListener?.invoke() + } + ).execute() + } } fun deleteAll() { + attachService { + mBinder?.deleteAll() + } IOActionTask( { cipherDatabaseDao.deleteAll() diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt index 4222ff5d3..7c51f70ee 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/CipherDatabaseEntity.kt @@ -43,6 +43,11 @@ data class CipherDatabaseEntity( parcel.readString()!!, parcel.readString()!!) + fun replaceContent(copy: CipherDatabaseEntity) { + this.encryptedValue = copy.encryptedValue + this.specParameters = copy.specParameters + } + override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(databaseUri) parcel.writeString(encryptedValue) diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt index 6b8c22781..9c7242fb9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt @@ -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() ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt index 28bd18ed7..4cc0914d4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt @@ -34,7 +34,12 @@ class IOActionTask( mainScope.launch { withContext(Dispatchers.IO) { val asyncResult: Deferred = async { - action.invoke() + try { + action.invoke() + } catch (e: Exception) { + e.printStackTrace() + null + } } withContext(Dispatchers.Main) { afterActionDatabaseListener?.invoke(asyncResult.await()) diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt new file mode 100644 index 000000000..2043b9705 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt @@ -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?) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index 5a1a64d1d..aba7353a8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -19,18 +19,27 @@ */ package com.kunzisoft.keepass.autofill +import android.annotation.SuppressLint import android.app.Activity +import android.app.PendingIntent import android.app.assist.AssistStructure import android.content.Context import android.content.Intent +import android.graphics.BlendMode +import android.graphics.drawable.Icon import android.os.Build import android.service.autofill.Dataset import android.service.autofill.FillResponse +import android.service.autofill.InlinePresentation import android.util.Log import android.view.autofill.AutofillManager import android.view.autofill.AutofillValue +import android.view.inputmethod.InlineSuggestionsRequest import android.widget.RemoteViews +import android.widget.Toast import androidx.annotation.RequiresApi +import androidx.autofill.inline.UiVersions +import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.core.content.ContextCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper @@ -38,8 +47,11 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.icons.assignDatabaseIcon +import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo +import com.kunzisoft.keepass.settings.AutofillSettingsActivity +import com.kunzisoft.keepass.settings.PreferencesUtil @RequiresApi(api = Build.VERSION_CODES.O) @@ -47,11 +59,17 @@ object AutofillHelper { private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165 - private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE + private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE + const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST" - fun retrieveAssistStructure(intent: Intent?): AssistStructure? { - intent?.let { - return it.getParcelableExtra(ASSIST_STRUCTURE) + fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { + intent?.getParcelableExtra(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AutofillComponent(assistStructure, + intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST)) + } else { + AutofillComponent(assistStructure, null) + } } return null } @@ -68,26 +86,10 @@ object AutofillHelper { return "" } - internal fun addHeader(responseBuilder: FillResponse.Builder, - packageName: String, - webDomain: String?, - applicationId: String?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if (webDomain != null) { - responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply { - setTextViewText(R.id.autofill_web_domain_text, webDomain) - }) - } else if (applicationId != null) { - responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply { - setTextViewText(R.id.autofill_app_id_text, applicationId) - }) - } - } - } - - internal fun buildDataset(context: Context, + private fun buildDataset(context: Context, entryInfo: EntryInfo, - struct: StructureParser.Result): Dataset? { + struct: StructureParser.Result, + inlinePresentation: InlinePresentation?): Dataset? { val title = makeEntryTitle(entryInfo) val views = newRemoteViews(context, title, entryInfo.icon) val builder = Dataset.Builder(views) @@ -100,6 +102,12 @@ object AutofillHelper { builder.setValue(password, AutofillValue.forText(entryInfo.password)) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + inlinePresentation?.let { + builder.setInlinePresentation(it) + } + } + return try { builder.build() } catch (e: IllegalArgumentException) { @@ -108,44 +116,120 @@ object AutofillHelper { } } + @RequiresApi(Build.VERSION_CODES.R) + @SuppressLint("RestrictedApi") + private fun buildInlinePresentationForEntry(context: Context, + inlineSuggestionsRequest: InlineSuggestionsRequest, + positionItem: Int, + entryInfo: EntryInfo): InlinePresentation? { + val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs + val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount + + if (positionItem <= maxSuggestion-1 + && inlinePresentationSpecs.size > positionItem) { + val inlinePresentationSpec = inlinePresentationSpecs[positionItem] + + // Make sure that the IME spec claims support for v1 UI template. + val imeStyle = inlinePresentationSpec.style + if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) + return null + + // Build the content for IME UI + val pendingIntent = PendingIntent.getActivity(context, + 0, + Intent(context, AutofillSettingsActivity::class.java), + 0) + return InlinePresentation( + InlineSuggestionUi.newContentBuilder(pendingIntent).apply { + setContentDescription(context.getString(R.string.autofill_sign_in_prompt)) + setTitle(entryInfo.title) + setSubtitle(entryInfo.username) + setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply { + setTintBlendMode(BlendMode.DST) + }) + buildIconFromEntry(context, entryInfo)?.let { icon -> + setEndIcon(icon.apply { + setTintBlendMode(BlendMode.DST) + }) + } + }.build().slice, inlinePresentationSpec, false) + } + return null + } + + fun buildResponse(context: Context, + entriesInfo: List, + 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().apply { add(entryInfo) }) + fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) { + buildResponseAndSetResult(activity, ArrayList().apply { add(entryInfo) }) } /** * Build the Autofill response for many entry */ - fun buildResponse(activity: Activity, entriesInfo: List) { + fun buildResponseAndSetResult(activity: Activity, entriesInfo: List) { 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(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(EXTRA_ASSIST_STRUCTURE)?.let { structure -> + StructureParser(structure).parse()?.let { result -> + // New Response + val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val inlineSuggestionsRequest = activity.intent?.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST) + if (inlineSuggestionsRequest != null) { + Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() } + buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest) + } else { + buildResponse(activity, entriesInfo, result, null) } + val mReplyIntent = Intent() + Log.d(activity.javaClass.name, "Successed Autofill auth.") + mReplyIntent.putExtra( + AutofillManager.EXTRA_AUTHENTICATION_RESULT, + response) + setResultOk = true + activity.setResult(Activity.RESULT_OK, mReplyIntent) } - if (!setResultOk) { - Log.w(activity.javaClass.name, "Failed Autofill auth.") - activity.setResult(Activity.RESULT_CANCELED) - } + } + if (!setResultOk) { + Log.w(activity.javaClass.name, "Failed Autofill auth.") + activity.setResult(Activity.RESULT_CANCELED) } } } @@ -155,10 +239,16 @@ object AutofillHelper { */ fun startActivityForAutofillResult(activity: Activity, intent: Intent, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo?) { EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION) - intent.putExtra(ASSIST_STRUCTURE, assistStructure) + intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) { + autofillComponent.inlineSuggestionsRequest?.let { + intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) + } + } EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo) activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) } @@ -192,4 +282,11 @@ object AutofillHelper { } return presentation } + + private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? { + return createIconFromDatabaseIcon(context, + Database.getInstance().drawFactory, + entryInfo.icon, + ContextCompat.getColor(context, R.color.green)) + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt index 70801c75c..cdd6e11a6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -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? = null var webDomainBlocklist: Set? = null var askToSaveData: Boolean = false + var autofillInlineSuggestionsEnabled: Boolean = false private var mLock = AtomicBoolean() override fun onCreate() { super.onCreate() + getPreferences() + } + private fun getPreferences() { applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this) webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this) - askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed + askToSaveData = PreferencesUtil.askToSaveAutofillData(this) + autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this) } override fun onFillRequest(request: FillRequest, @@ -75,7 +88,16 @@ class KeeAutofillService : AutofillService() { } SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> searchInfo.webDomain = webDomainWithoutSubDomain - launchSelection(searchInfo, parseResult, callback) + val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && autofillInlineSuggestionsEnabled) { + request.inlineSuggestionsRequest + } else { + null + } + launchSelection(searchInfo, + parseResult, + inlineSuggestionsRequest, + callback) } } } @@ -84,39 +106,40 @@ class KeeAutofillService : AutofillService() { private fun launchSelection(searchInfo: SearchInfo, parseResult: StructureParser.Result, + inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { SearchHelper.checkAutoSearchInfo(this, Database.getInstance(), searchInfo, { items -> - val responseBuilder = FillResponse.Builder() - AutofillHelper.addHeader(responseBuilder, packageName, - parseResult.webDomain, parseResult.applicationId) - items.forEach { - responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult)) - } - callback.onSuccess(responseBuilder.build()) + callback.onSuccess( + AutofillHelper.buildResponse(this, + items, parseResult, inlineSuggestionsRequest) + ) }, { // Show UI if no search result - showUIForEntrySelection(parseResult, searchInfo, callback) + showUIForEntrySelection(parseResult, + searchInfo, inlineSuggestionsRequest, callback) }, { // Show UI if database not open - showUIForEntrySelection(parseResult, searchInfo, callback) + showUIForEntrySelection(parseResult, + searchInfo, inlineSuggestionsRequest, callback) } ) } private fun showUIForEntrySelection(parseResult: StructureParser.Result, searchInfo: SearchInfo, + inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { parseResult.allAutofillIds().let { autofillIds -> if (autofillIds.isNotEmpty()) { // If the entire Autofill Response is authenticated, AuthActivity is used // to generate Response. val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this, - searchInfo) + searchInfo, inlineSuggestionsRequest) val responseBuilder = FillResponse.Builder() val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) { RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply { @@ -149,7 +172,40 @@ class KeeAutofillService : AutofillService() { ) } } - // Build response + + // Build inline presentation + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && autofillInlineSuggestionsEnabled) { + var inlinePresentation: InlinePresentation? = null + inlineSuggestionsRequest?.let { + val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs + if (inlineSuggestionsRequest.maxSuggestionCount > 0 + && inlinePresentationSpecs.size > 0) { + val inlinePresentationSpec = inlinePresentationSpecs[0] + + // Make sure that the IME spec claims support for v1 UI template. + val imeStyle = inlinePresentationSpec.style + if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) { + // Build the content for IME UI + inlinePresentation = InlinePresentation( + InlineSuggestionUi.newContentBuilder( + PendingIntent.getActivity(this, + 0, + Intent(this, AutofillSettingsActivity::class.java), + 0) + ).apply { + setContentDescription(getString(R.string.autofill_sign_in_prompt)) + setTitle(getString(R.string.autofill_sign_in_prompt)) + setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply { + setTintBlendMode(BlendMode.DST) + }) + }.build().slice, inlinePresentationSpec, false) + } + } + } + // Build response + responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation) + } responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock) callback.onSuccess(responseBuilder.build()) } @@ -190,6 +246,7 @@ class KeeAutofillService : AutofillService() { override fun onConnected() { Log.d(TAG, "onConnected") + getPreferences() } override fun onDisconnected() { diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index dd6ee4cb4..c93cb4a36 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -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 diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt new file mode 100644 index 000000000..26e9baff4 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockCryptoPrompt.kt @@ -0,0 +1,10 @@ +package com.kunzisoft.keepass.biometric + +import androidx.annotation.StringRes +import javax.crypto.Cipher + +data class AdvancedUnlockCryptoPrompt(var cipher: Cipher, + @StringRes var promptTitleId: Int, + @StringRes var promptDescriptionId: Int? = null, + var isDeviceCredentialOperation: Boolean, + var isBiometricOperation: Boolean) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt new file mode 100644 index 000000000..83b77a33b --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockFragment.kt @@ -0,0 +1,628 @@ +/* + * Copyright 2020 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.biometric + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import android.view.* +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import com.getkeepsafe.taptargetview.TapTargetView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.app.database.CipherDatabaseAction +import com.kunzisoft.keepass.database.exception.IODatabaseException +import com.kunzisoft.keepass.education.PasswordActivityEducation +import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.view.AdvancedUnlockInfoView + +class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback { + + private var mBuilderListener: BuilderListener? = null + + private var mAdvancedUnlockEnabled = false + private var mAutoOpenPromptEnabled = false + + private var advancedUnlockManager: AdvancedUnlockManager? = null + private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE + private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null + + var databaseFileUri: Uri? = null + private set + + /** + * Manage setting to auto open biometric prompt + */ + private var mAutoOpenPrompt: Boolean = false + get() { + return field && mAutoOpenPromptEnabled + } + + // Variable to check if the prompt can be open (if the right activity is currently shown) + // checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization + private var allowOpenBiometricPrompt = false + + private lateinit var cipherDatabaseAction : CipherDatabaseAction + + private var cipherDatabaseListener: CipherDatabaseAction.DatabaseListener? = null + + // Only to fix multiple fingerprint menu #332 + private var mAllowAdvancedUnlockMenu = false + private var mAddBiometricMenuInProgress = false + + // Only keep connection when we request a device credential activity + private var keepConnection = false + + override fun onAttach(context: Context) { + super.onAttach(context) + + mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context) + mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context) + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mBuilderListener = context as BuilderListener + } + } catch (e: ClassCastException) { + throw ClassCastException(context.toString() + + " must implement " + BuilderListener::class.java.name) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + retainInstance = true + setHasOptionsMenu(true) + + cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + + val rootView = inflater.cloneInContext(contextThemed) + .inflate(R.layout.fragment_advanced_unlock, container, false) + + mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view) + + return rootView + } + + private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?) + private var activityResult: ActivityResult? = null + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + // To wait resume + if (keepConnection) { + activityResult = ActivityResult(requestCode, resultCode, data) + } + keepConnection = false + } + + override fun onResume() { + super.onResume() + mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(requireContext()) + mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(requireContext()) + keepConnection = false + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // biometric menu + if (mAllowAdvancedUnlockMenu) + inflater.inflate(R.menu.advanced_unlock, menu) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + deleteEncryptedDatabaseKey() + } + } + + return super.onOptionsItemSelected(item) + } + + fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // To get device credential unlock result, only if same database uri + if (databaseUri != null + && mAdvancedUnlockEnabled) { + activityResult?.let { + if (databaseUri == databaseFileUri) { + advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode) + } else { + disconnect() + } + } ?: run { + this.mAutoOpenPrompt = autoOpenPrompt + connect(databaseUri) + } + } else { + disconnect() + } + activityResult = null + } + } + + /** + * Check unlock availability and change the current mode depending of device's state + */ + fun checkUnlockAvailability() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + allowOpenBiometricPrompt = true + if (PreferencesUtil.isBiometricUnlockEnable(requireContext())) { + mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint) + + // biometric not supported (by API level or hardware) so keep option hidden + // or manually disable + val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext()) + if (!PreferencesUtil.isAdvancedUnlockEnable(requireContext()) + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + toggleMode(Mode.BIOMETRIC_UNAVAILABLE) + } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) { + toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) + } else { + // biometric is available but not configured, show icon but in disabled state with some information + if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { + toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) + } else { + selectMode() + } + } + } else if (PreferencesUtil.isDeviceCredentialUnlockEnable(requireContext())) { + mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt) + if (AdvancedUnlockManager.isDeviceSecure(requireContext())) { + selectMode() + } else { + toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED) + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun selectMode() { + // Check if fingerprint well init (be called the first time the fingerprint is configured + // and the activity still active) + if (advancedUnlockManager?.isKeyManagerInitialized != true) { + advancedUnlockManager = AdvancedUnlockManager { requireActivity() } + // callback for fingerprint findings + advancedUnlockManager?.advancedUnlockCallback = this + } + // Recheck to change the mode + if (advancedUnlockManager?.isKeyManagerInitialized != true) { + toggleMode(Mode.KEY_MANAGER_UNAVAILABLE) + } else { + if (mBuilderListener?.conditionToStoreCredential() == true) { + // listen for encryption + toggleMode(Mode.STORE_CREDENTIAL) + } else { + databaseFileUri?.let { databaseUri -> + cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> + // biometric available but no stored password found yet for this DB so show info don't listen + toggleMode(if (containsCipher) { + // listen for decryption + Mode.EXTRACT_CREDENTIAL + } else { + // wait for typing + Mode.WAIT_CREDENTIAL + }) + } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun toggleMode(newBiometricMode: Mode) { + if (newBiometricMode != biometricMode) { + biometricMode = newBiometricMode + initAdvancedUnlockMode() + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initNotAvailable() { + showViews(false) + + mAdvancedUnlockInfoView?.setIconViewClickListener(false, null) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun openBiometricSetting() { + mAdvancedUnlockInfoView?.setIconViewClickListener(false) { + // ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices... + requireContext().startActivity(Intent(Settings.ACTION_SETTINGS)) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initSecurityUpdateRequired() { + showViews(true) + setAdvancedUnlockedTitleView(R.string.biometric_security_update_required) + + openBiometricSetting() + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initNotConfigured() { + showViews(true) + setAdvancedUnlockedTitleView(R.string.configure_biometric) + setAdvancedUnlockedMessageView("") + + openBiometricSetting() + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initKeyManagerNotAvailable() { + showViews(true) + setAdvancedUnlockedTitleView(R.string.keystore_not_accessible) + + openBiometricSetting() + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initWaitData() { + showViews(true) + setAdvancedUnlockedTitleView(R.string.no_credentials_stored) + setAdvancedUnlockedMessageView("") + + mAdvancedUnlockInfoView?.setIconViewClickListener(false) { + onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + requireContext().getString(R.string.credential_before_click_advanced_unlock_button)) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { + activity?.runOnUiThread { + if (allowOpenBiometricPrompt) { + if (cryptoPrompt.isDeviceCredentialOperation) + keepConnection = true + try { + advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt) + } catch (e: Exception) { + Log.e(TAG, "Unable to open advanced unlock prompt", e) + setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initEncryptData() { + showViews(true) + setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential) + setAdvancedUnlockedMessageView("") + + advancedUnlockManager?.initEncryptData { cryptoPrompt -> + // Set listener to open the biometric dialog and save credential + mAdvancedUnlockInfoView?.setIconViewClickListener { _ -> + openAdvancedUnlockPrompt(cryptoPrompt) + } + } ?: throw Exception("AdvancedUnlockManager not initialized") + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun initDecryptData() { + showViews(true) + setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database) + setAdvancedUnlockedMessageView("") + + advancedUnlockManager?.let { unlockHelper -> + databaseFileUri?.let { databaseUri -> + cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> + cipherDatabase?.let { + unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt -> + + // Set listener to open the biometric dialog and check credential + mAdvancedUnlockInfoView?.setIconViewClickListener { _ -> + openAdvancedUnlockPrompt(cryptoPrompt) + } + + // Auto open the biometric prompt + if (mAutoOpenPrompt) { + mAutoOpenPrompt = false + openAdvancedUnlockPrompt(cryptoPrompt) + } + } + } ?: deleteEncryptedDatabaseKey() + } + } ?: throw IODatabaseException() + } ?: throw Exception("AdvancedUnlockManager not initialized") + } + + @Synchronized + fun initAdvancedUnlockMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mAllowAdvancedUnlockMenu = false + try { + when (biometricMode) { + Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable() + Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired() + Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured() + Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable() + Mode.WAIT_CREDENTIAL -> initWaitData() + Mode.STORE_CREDENTIAL -> initEncryptData() + Mode.EXTRACT_CREDENTIAL -> initDecryptData() + } + } catch (e: Exception) { + onGenericException(e) + } + invalidateBiometricMenu() + } + } + + private fun invalidateBiometricMenu() { + // Show fingerprint key deletion + if (!mAddBiometricMenuInProgress) { + mAddBiometricMenuInProgress = true + databaseFileUri?.let { databaseUri -> + cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher -> + mAllowAdvancedUnlockMenu = containsCipher + && (biometricMode != Mode.BIOMETRIC_UNAVAILABLE + && biometricMode != Mode.KEY_MANAGER_UNAVAILABLE) + mAddBiometricMenuInProgress = false + activity?.invalidateOptionsMenu() + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun connect(databaseUri: Uri) { + showViews(true) + this.databaseFileUri = databaseUri + cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener { + override fun onDatabaseCleared() { + deleteEncryptedDatabaseKey() + } + } + cipherDatabaseAction.apply { + reloadPreferences() + cipherDatabaseListener?.let { + registerDatabaseListener(it) + } + } + checkUnlockAvailability() + } + + @RequiresApi(Build.VERSION_CODES.M) + fun disconnect(hideViews: Boolean = true, + closePrompt: Boolean = true) { + this.databaseFileUri = null + // Close the biometric prompt + allowOpenBiometricPrompt = false + if (closePrompt) + advancedUnlockManager?.closeBiometricPrompt() + cipherDatabaseListener?.let { + cipherDatabaseAction.unregisterDatabaseListener(it) + } + biometricMode = Mode.BIOMETRIC_UNAVAILABLE + if (hideViews) { + showViews(false) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun deleteEncryptedDatabaseKey() { + allowOpenBiometricPrompt = false + mAdvancedUnlockInfoView?.setIconViewClickListener(false, null) + advancedUnlockManager?.closeBiometricPrompt() + databaseFileUri?.let { databaseUri -> + cipherDatabaseAction.deleteByDatabaseUri(databaseUri) { + checkUnlockAvailability() + } + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + activity?.runOnUiThread { + Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") + setAdvancedUnlockedMessageView(errString.toString()) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onAuthenticationFailed() { + activity?.runOnUiThread { + Log.e(TAG, "Biometric authentication failed, biometric not recognized") + setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onAuthenticationSucceeded() { + activity?.runOnUiThread { + when (biometricMode) { + Mode.BIOMETRIC_UNAVAILABLE -> { + } + Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> { + } + Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> { + } + Mode.KEY_MANAGER_UNAVAILABLE -> { + } + Mode.WAIT_CREDENTIAL -> { + } + Mode.STORE_CREDENTIAL -> { + // newly store the entered password in encrypted way + mBuilderListener?.retrieveCredentialForEncryption()?.let { credential -> + advancedUnlockManager?.encryptData(credential) + } + AdvancedUnlockNotificationService.startServiceForTimeout(requireContext()) + } + Mode.EXTRACT_CREDENTIAL -> { + // retrieve the encrypted value from preferences + databaseFileUri?.let { databaseUri -> + cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase -> + cipherDatabase?.encryptedValue?.let { value -> + advancedUnlockManager?.decryptData(value) + } ?: deleteEncryptedDatabaseKey() + } + } ?: run { + onAuthenticationError(-1, getString(R.string.error_database_uri_null)) + } + } + } + } + } + + override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) { + databaseFileUri?.let { databaseUri -> + mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec) + } + } + + override fun handleDecryptedResult(decryptedValue: String) { + // Load database directly with password retrieve + databaseFileUri?.let { + mBuilderListener?.onCredentialDecrypted(it, decryptedValue) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onInvalidKeyException(e: Exception) { + setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) + } + + override fun onGenericException(e: Exception) { + val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" + setAdvancedUnlockedMessageView(errorMessage) + } + + private fun showViews(show: Boolean) { + activity?.runOnUiThread { + mAdvancedUnlockInfoView?.visibility = if (show) + View.VISIBLE + else { + View.GONE + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun setAdvancedUnlockedTitleView(textId: Int) { + activity?.runOnUiThread { + mAdvancedUnlockInfoView?.setTitle(textId) + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun setAdvancedUnlockedMessageView(textId: Int) { + activity?.runOnUiThread { + mAdvancedUnlockInfoView?.setMessage(textId) + } + } + + private fun setAdvancedUnlockedMessageView(text: CharSequence) { + activity?.runOnUiThread { + mAdvancedUnlockInfoView?.message = text + } + } + + fun performEducation(passwordActivityEducation: PasswordActivityEducation, + readOnlyEducationPerformed: Boolean, + onEducationViewClick: ((TapTargetView?) -> Unit)? = null, + onOuterViewClick: ((TapTargetView?) -> Unit)? = null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && !readOnlyEducationPerformed) { + val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext()) + PreferencesUtil.isAdvancedUnlockEnable(requireContext()) + && (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) + && mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE + && mAdvancedUnlockInfoView?.unlockIconImageView != null + && passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!, + onEducationViewClick, + onOuterViewClick) + } + } catch (ignored: Exception) {} + } + + enum class Mode { + BIOMETRIC_UNAVAILABLE, + BIOMETRIC_SECURITY_UPDATE_REQUIRED, + DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED, + KEY_MANAGER_UNAVAILABLE, + WAIT_CREDENTIAL, + STORE_CREDENTIAL, + EXTRACT_CREDENTIAL + } + + interface BuilderListener { + fun retrieveCredentialForEncryption(): String + fun conditionToStoreCredential(): Boolean + fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String) + fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String) + } + + override fun onPause() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!keepConnection) { + // If close prompt, bug "user not authenticated in Android R" + disconnect(false) + advancedUnlockManager = null + } + } + + super.onPause() + } + + override fun onDestroyView() { + mAdvancedUnlockInfoView = null + + super.onDestroyView() + } + + override fun onDestroy() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + disconnect() + advancedUnlockManager = null + mBuilderListener = null + } + + super.onDestroy() + } + + override fun onDetach() { + mBuilderListener = null + + super.onDetach() + } + + companion object { + + private val TAG = AdvancedUnlockFragment::class.java.name + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt similarity index 51% rename from app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt rename to app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt index 65700bfbd..c183bc893 100644 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/BiometricUnlockDatabaseHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Jeremy Jamet / Kunzisoft. + * Copyright 2020 Jeremy Jamet / Kunzisoft. * * This file is part of KeePassDX. * @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.biometric +import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.os.Build @@ -31,6 +32,7 @@ import androidx.annotation.RequiresApi import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.* import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import com.kunzisoft.keepass.R import com.kunzisoft.keepass.settings.PreferencesUtil @@ -44,48 +46,74 @@ import javax.crypto.SecretKey import javax.crypto.spec.IvParameterSpec @RequiresApi(api = Build.VERSION_CODES.M) -class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { - - private var biometricPrompt: BiometricPrompt? = null +class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) { private var keyStore: KeyStore? = null private var keyGenerator: KeyGenerator? = null private var cipher: Cipher? = null - private var keyguardManager: KeyguardManager? = null - private var cryptoObject: BiometricPrompt.CryptoObject? = null + + private var biometricPrompt: BiometricPrompt? = null + private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + advancedUnlockCallback?.onAuthenticationSucceeded() + } + + override fun onAuthenticationFailed() { + advancedUnlockCallback?.onAuthenticationFailed() + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + advancedUnlockCallback?.onAuthenticationError(errorCode, errString) + } + } + + var advancedUnlockCallback: AdvancedUnlockCallback? = null private var isKeyManagerInit = false - var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null - var biometricUnlockCallback: BiometricUnlockCallback? = null - private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(context) + private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext()) + private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext()) val isKeyManagerInitialized: Boolean get() { if (!isKeyManagerInit) { - biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized")) + advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized")) } return isKeyManagerInit } + private fun isBiometricOperation(): Boolean { + return biometricUnlockEnable || isDeviceCredentialBiometricOperation() + } + + // Since Android 30, device credential is also a biometric operation + private fun isDeviceCredentialOperation(): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.R + && deviceCredentialUnlockEnable + } + + private fun isDeviceCredentialBiometricOperation(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && deviceCredentialUnlockEnable + } + init { - if (allowInitKeyStore(context)) { - this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager? + if (isDeviceSecure(retrieveContext()) + && (biometricUnlockEnable || deviceCredentialUnlockEnable)) { try { - this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE) - this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE) + this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE) + this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE) this.cipher = Cipher.getInstance( - BIOMETRIC_KEY_ALGORITHM + "/" - + BIOMETRIC_BLOCKS_MODES + "/" - + BIOMETRIC_ENCRYPTION_PADDING) - this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!) + ADVANCED_UNLOCK_KEY_ALGORITHM + "/" + + ADVANCED_UNLOCK_BLOCKS_MODES + "/" + + ADVANCED_UNLOCK_ENCRYPTION_PADDING) isKeyManagerInit = (keyStore != null && keyGenerator != null && cipher != null) } catch (e: Exception) { Log.e(TAG, "Unable to initialize the keystore", e) isKeyManagerInit = false - biometricUnlockCallback?.onBiometricException(e) + advancedUnlockCallback?.onGenericException(e) } } else { // really not much to do when no fingerprint support found @@ -103,22 +131,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { keyStore.load(null) try { - if (!keyStore.containsAlias(BIOMETRIC_KEYSTORE_KEY)) { + if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) { // Set the alias of the entry in Android KeyStore where the key will appear // and the constrains (purposes) in the constructor of the Builder keyGenerator?.init( KeyGenParameterSpec.Builder( - BIOMETRIC_KEYSTORE_KEY, + ADVANCED_UNLOCK_KEYSTORE_KEY, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) // Require the user to authenticate with a fingerprint to authorize every use - // of the key - .setUserAuthenticationRequired(true) + // of the key, don't use it for device credential because it's the user authentication .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - && deviceCredentialUnlockEnable) { - setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL) + if (biometricUnlockEnable) { + setUserAuthenticationRequired(true) } } .build()) @@ -126,56 +152,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { } } catch (e: Exception) { Log.e(TAG, "Unable to create a key in keystore", e) - biometricUnlockCallback?.onBiometricException(e) + advancedUnlockCallback?.onGenericException(e) } - return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey? + return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey? } } catch (e: Exception) { Log.e(TAG, "Unable to retrieve the key in keystore", e) - biometricUnlockCallback?.onBiometricException(e) + advancedUnlockCallback?.onGenericException(e) } return null } fun initEncryptData(actionIfCypherInit - : (biometricPrompt: BiometricPrompt?, - cryptoObject: BiometricPrompt.CryptoObject?, - promptInfo: BiometricPrompt.PromptInfo) -> Unit) { + : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { if (!isKeyManagerInitialized) { return } try { - // TODO if (keyguardManager?.isDeviceSecure == true) { getSecretKey()?.let { secretKey -> - cipher?.init(Cipher.ENCRYPT_MODE, secretKey) + cipher?.let { cipher -> + cipher.init(Cipher.ENCRYPT_MODE, secretKey) - initBiometricPrompt() - - val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply { - setTitle(context.getString(R.string.advanced_unlock_prompt_store_credential_title)) - setDescription(context.getString(R.string.advanced_unlock_prompt_store_credential_message)) - setConfirmationRequired(true) - if (deviceCredentialUnlockEnable) { - setAllowedAuthenticators(DEVICE_CREDENTIAL) - } else { - setNegativeButtonText(context.getString(android.R.string.cancel)) - } - }.build() - - actionIfCypherInit.invoke(biometricPrompt, - cryptoObject, - promptInfoStoreCredential) + actionIfCypherInit.invoke( + AdvancedUnlockCryptoPrompt( + cipher, + R.string.advanced_unlock_prompt_store_credential_title, + R.string.advanced_unlock_prompt_store_credential_message, + isDeviceCredentialOperation(), isBiometricOperation()) + ) + } } } catch (unrecoverableKeyException: UnrecoverableKeyException) { Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException) - biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException) + advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException) } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException) - biometricUnlockCallback?.onInvalidKeyException(invalidKeyException) + advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) } catch (e: Exception) { Log.e(TAG, "Unable to initialize encrypt data", e) - biometricUnlockCallback?.onBiometricException(e) + advancedUnlockCallback?.onGenericException(e) } } @@ -190,57 +206,46 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { // passes updated iv spec on to callback so this can be stored for decryption cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec -> val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP) - biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue) + advancedUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue) } } catch (e: Exception) { - val exception = Exception(context.getString(R.string.keystore_not_accessible), e) Log.e(TAG, "Unable to encrypt data", e) - biometricUnlockCallback?.onBiometricException(exception) + advancedUnlockCallback?.onGenericException(e) } } fun initDecryptData(ivSpecValue: String, actionIfCypherInit - : (biometricPrompt: BiometricPrompt?, - cryptoObject: BiometricPrompt.CryptoObject?, - promptInfo: BiometricPrompt.PromptInfo) -> Unit) { + : (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) { if (!isKeyManagerInitialized) { return } try { - // TODO if (keyguardManager?.isDeviceSecure == true) { // important to restore spec here that was used for decryption val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP) val spec = IvParameterSpec(iv) getSecretKey()?.let { secretKey -> - cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec) + cipher?.let { cipher -> + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) - initBiometricPrompt() - - val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply { - setTitle(context.getString(R.string.advanced_unlock_prompt_extract_credential_title)) - //setDescription(context.getString(R.string.biometric_prompt_extract_credential_message)) - setConfirmationRequired(false) - if (deviceCredentialUnlockEnable) { - setAllowedAuthenticators(DEVICE_CREDENTIAL) - } else { - setNegativeButtonText(context.getString(android.R.string.cancel)) - } - }.build() - - actionIfCypherInit.invoke(biometricPrompt, - cryptoObject, - promptInfoExtractCredential) + actionIfCypherInit.invoke( + AdvancedUnlockCryptoPrompt( + cipher, + R.string.advanced_unlock_prompt_extract_credential_title, + null, + isDeviceCredentialOperation(), isBiometricOperation()) + ) + } } } catch (unrecoverableKeyException: UnrecoverableKeyException) { Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException) - deleteEntryKey() + deleteKeystoreKey() } catch (invalidKeyException: KeyPermanentlyInvalidatedException) { Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException) - biometricUnlockCallback?.onInvalidKeyException(invalidKeyException) + advancedUnlockCallback?.onInvalidKeyException(invalidKeyException) } catch (e: Exception) { Log.e(TAG, "Unable to initialize decrypt data", e) - biometricUnlockCallback?.onBiometricException(e) + advancedUnlockCallback?.onGenericException(e) } } @@ -252,33 +257,73 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { // actual decryption here val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP) cipher?.doFinal(encrypted)?.let { decrypted -> - biometricUnlockCallback?.handleDecryptedResult(String(decrypted)) + advancedUnlockCallback?.handleDecryptedResult(String(decrypted)) } } catch (badPaddingException: BadPaddingException) { Log.e(TAG, "Unable to decrypt data", badPaddingException) - biometricUnlockCallback?.onInvalidKeyException(badPaddingException) + advancedUnlockCallback?.onInvalidKeyException(badPaddingException) } catch (e: Exception) { - val exception = Exception(context.getString(R.string.keystore_not_accessible), e) - Log.e(TAG, "Unable to decrypt data", exception) - biometricUnlockCallback?.onBiometricException(exception) + Log.e(TAG, "Unable to decrypt data", e) + advancedUnlockCallback?.onGenericException(e) } } - fun deleteEntryKey() { + fun deleteKeystoreKey() { try { keyStore?.load(null) - keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY) + keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY) } catch (e: Exception) { Log.e(TAG, "Unable to delete entry key in keystore", e) - biometricUnlockCallback?.onBiometricException(e) + advancedUnlockCallback?.onGenericException(e) + } + } + + @Suppress("DEPRECATION") + @Synchronized + fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) { + // Init advanced unlock prompt + if (biometricPrompt == null) { + biometricPrompt = BiometricPrompt(retrieveContext(), + Executors.newSingleThreadExecutor(), + authenticationCallback) + } + + val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId) + val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId -> + retrieveContext().getString(descriptionId) + } ?: "" + + if (cryptoPrompt.isBiometricOperation) { + val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply { + setTitle(promptTitle) + if (promptDescription.isNotEmpty()) + setDescription(promptDescription) + setConfirmationRequired(false) + if (isDeviceCredentialBiometricOperation()) { + setAllowedAuthenticators(DEVICE_CREDENTIAL) + } else { + setNegativeButtonText(retrieveContext().getString(android.R.string.cancel)) + } + }.build() + biometricPrompt?.authenticate( + promptInfoExtractCredential, + BiometricPrompt.CryptoObject(cryptoPrompt.cipher)) + } + else if (cryptoPrompt.isDeviceCredentialOperation) { + val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java) + retrieveContext().startActivityForResult( + keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription), + REQUEST_DEVICE_CREDENTIAL) } } @Synchronized - fun initBiometricPrompt() { - if (biometricPrompt == null) { - authenticationCallback?.let { - biometricPrompt = BiometricPrompt(context, Executors.newSingleThreadExecutor(), it) + fun onActivityResult(requestCode: Int, resultCode: Int) { + if (requestCode == REQUEST_DEVICE_CREDENTIAL) { + if (resultCode == Activity.RESULT_OK) { + advancedUnlockCallback?.onAuthenticationSucceeded() + } else { + advancedUnlockCallback?.onAuthenticationFailed() } } } @@ -287,25 +332,30 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { biometricPrompt?.cancelAuthentication() } - interface BiometricUnlockErrorCallback { + interface AdvancedUnlockErrorCallback { fun onInvalidKeyException(e: Exception) - fun onBiometricException(e: Exception) + fun onGenericException(e: Exception) } - interface BiometricUnlockCallback : BiometricUnlockErrorCallback { + interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback { + fun onAuthenticationSucceeded() + fun onAuthenticationFailed() + fun onAuthenticationError(errorCode: Int, errString: CharSequence) fun handleEncryptedResult(encryptedValue: String, ivSpec: String) fun handleDecryptedResult(decryptedValue: String) } companion object { - private val TAG = BiometricUnlockDatabaseHelper::class.java.name + private val TAG = AdvancedUnlockManager::class.java.name - private const val BIOMETRIC_KEYSTORE = "AndroidKeyStore" - private const val BIOMETRIC_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key" - private const val BIOMETRIC_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES - private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC - private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 + private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore" + private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key" + private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC + private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 + + private const val REQUEST_DEVICE_CREDENTIAL = 556 @RequiresApi(api = Build.VERSION_CODES.M) fun canAuthenticate(context: Context): Int { @@ -337,11 +387,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { } @RequiresApi(api = Build.VERSION_CODES.M) - fun allowInitKeyStore(context: Context): Boolean { - val biometricCanAuthenticate = canAuthenticate(context) - return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN - ) + fun isDeviceSecure(context: Context): Boolean { + val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java) + return keyguardManager?.isDeviceSecure ?: false } @RequiresApi(api = Build.VERSION_CODES.M) @@ -365,39 +413,51 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) { ) } - @RequiresApi(api = Build.VERSION_CODES.R) + @RequiresApi(api = Build.VERSION_CODES.M) fun deviceCredentialUnlockSupported(context: Context): Boolean { - val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL) - return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL) + return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply { + return isDeviceSecure + } + } + return false } /** * Remove entry key in keystore */ @RequiresApi(api = Build.VERSION_CODES.M) - fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity, - biometricCallback: BiometricUnlockErrorCallback) { - BiometricUnlockDatabaseHelper(context).apply { - biometricUnlockCallback = object : BiometricUnlockCallback { + fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity, + advancedCallback: AdvancedUnlockErrorCallback) { + AdvancedUnlockManager{ fragmentActivity }.apply { + advancedUnlockCallback = object : AdvancedUnlockCallback { + override fun onAuthenticationSucceeded() {} + + override fun onAuthenticationFailed() {} + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {} override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {} override fun handleDecryptedResult(decryptedValue: String) {} override fun onInvalidKeyException(e: Exception) { - biometricCallback.onInvalidKeyException(e) + advancedCallback.onInvalidKeyException(e) } - override fun onBiometricException(e: Exception) { - biometricCallback.onBiometricException(e) + override fun onGenericException(e: Exception) { + advancedCallback.onGenericException(e) } } - deleteEntryKey() + deleteKeystoreKey() } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt b/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt deleted file mode 100644 index daf882b38..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/biometric/AdvancedUnlockedManager.kt +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePassDX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePassDX. If not, see . - * - */ -package com.kunzisoft.keepass.biometric - -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.provider.Settings -import android.util.Log -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.widget.CompoundButton -import android.widget.TextView -import androidx.annotation.RequiresApi -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.fragment.app.FragmentActivity -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.app.database.CipherDatabaseAction -import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.view.AdvancedUnlockInfoView - -@RequiresApi(api = Build.VERSION_CODES.M) -class AdvancedUnlockedManager(var context: FragmentActivity, - var databaseFileUri: Uri, - private var advancedUnlockInfoView: AdvancedUnlockInfoView?, - private var checkboxPasswordView: CompoundButton?, - private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null, - var passwordView: TextView?, - private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit, - private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit) - : BiometricUnlockDatabaseHelper.BiometricUnlockCallback { - - private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null - private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE - - // Only to fix multiple fingerprint menu #332 - private var mAllowAdvancedUnlockMenu = false - private var mAddBiometricMenuInProgress = false - - /** - * Manage setting to auto open biometric prompt - */ - private var biometricPromptAutoOpenPreference = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context) - var isBiometricPromptAutoOpenEnable: Boolean = false - get() { - return field && biometricPromptAutoOpenPreference - } - - // Variable to check if the prompt can be open (if the right activity is currently shown) - // checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization - private var allowOpenBiometricPrompt = false - - private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext) - - init { - // Add a check listener to change fingerprint mode - checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked -> - checkBiometricAvailability() - // Add old listener to enable the button, only be call here because of onCheckedChange bug - onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked) - } - } - - /** - * Check biometric availability and change the current mode depending of device's state - */ - fun checkBiometricAvailability() { - - if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) { - advancedUnlockInfoView?.setIconResource(R.drawable.bolt) - } else if (PreferencesUtil.isBiometricUnlockEnable(context)) { - advancedUnlockInfoView?.setIconResource(R.drawable.fingerprint) - } - - // biometric not supported (by API level or hardware) so keep option hidden - // or manually disable - val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context) - allowOpenBiometricPrompt = true - - if (!PreferencesUtil.isAdvancedUnlockEnable(context) - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - || biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { - toggleMode(Mode.BIOMETRIC_UNAVAILABLE) - } else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){ - toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED) - } else { - // biometric is available but not configured, show icon but in disabled state with some information - if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { - toggleMode(Mode.BIOMETRIC_NOT_CONFIGURED) - } else { - // Check if fingerprint well init (be called the first time the fingerprint is configured - // and the activity still active) - if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) { - biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context) - // callback for fingerprint findings - biometricUnlockDatabaseHelper?.biometricUnlockCallback = this - biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback - } - // Recheck to change the mode - if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) { - toggleMode(Mode.KEY_MANAGER_UNAVAILABLE) - } else { - if (checkboxPasswordView?.isChecked == true) { - // listen for encryption - toggleMode(Mode.STORE_CREDENTIAL) - } else { - cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher -> - // biometric available but no stored password found yet for this DB so show info don't listen - toggleMode(if (containsCipher) { - // listen for decryption - Mode.EXTRACT_CREDENTIAL - } else { - // wait for typing - Mode.WAIT_CREDENTIAL - }) - } - } - } - } - } - } - - private fun toggleMode(newBiometricMode: Mode) { - if (newBiometricMode != biometricMode) { - biometricMode = newBiometricMode - initAdvancedUnlockMode() - } - } - - private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () { - - override fun onAuthenticationError( - errorCode: Int, - errString: CharSequence) { - context.runOnUiThread { - Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString") - setAdvancedUnlockedMessageView(errString.toString()) - } - } - - override fun onAuthenticationFailed() { - context.runOnUiThread { - Log.e(TAG, "Biometric authentication failed, biometric not recognized") - setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized) - } - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - context.runOnUiThread { - when (biometricMode) { - Mode.BIOMETRIC_UNAVAILABLE -> { - } - Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> { - } - Mode.BIOMETRIC_NOT_CONFIGURED -> { - } - Mode.KEY_MANAGER_UNAVAILABLE -> { - } - Mode.WAIT_CREDENTIAL -> { - } - Mode.STORE_CREDENTIAL -> { - // newly store the entered password in encrypted way - biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString()) - } - Mode.EXTRACT_CREDENTIAL -> { - // retrieve the encrypted value from preferences - cipherDatabaseAction.getCipherDatabase(databaseFileUri) { - it?.encryptedValue?.let { value -> - biometricUnlockDatabaseHelper?.decryptData(value) - } - } - } - } - } - } - } - - private fun initNotAvailable() { - showFingerPrintViews(false) - - advancedUnlockInfoView?.setIconViewClickListener(false, null) - } - - private fun initSecurityUpdateRequired() { - showFingerPrintViews(true) - setAdvancedUnlockedTitleView(R.string.biometric_security_update_required) - - advancedUnlockInfoView?.setIconViewClickListener(false) { - context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) - } - } - - private fun initNotConfigured() { - showFingerPrintViews(true) - setAdvancedUnlockedTitleView(R.string.configure_biometric) - setAdvancedUnlockedMessageView("") - - advancedUnlockInfoView?.setIconViewClickListener(false) { - context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) - } - } - - private fun initKeyManagerNotAvailable() { - showFingerPrintViews(true) - setAdvancedUnlockedTitleView(R.string.keystore_not_accessible) - - advancedUnlockInfoView?.setIconViewClickListener(false) { - context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) - } - } - - private fun initWaitData() { - showFingerPrintViews(true) - setAdvancedUnlockedTitleView(R.string.no_credentials_stored) - setAdvancedUnlockedMessageView("") - - advancedUnlockInfoView?.setIconViewClickListener(false) { - biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, - context.getString(R.string.credential_before_click_advanced_unlock_button)) - } - } - - private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?, - cryptoObject: BiometricPrompt.CryptoObject?, - promptInfo: BiometricPrompt.PromptInfo) { - context.runOnUiThread { - if (allowOpenBiometricPrompt) { - if (biometricPrompt != null) { - if (cryptoObject != null) { - biometricPrompt.authenticate(promptInfo, cryptoObject) - } else { - setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized) - } - } else { - setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized) - } - } - } - } - - private fun initEncryptData() { - showFingerPrintViews(true) - setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential) - setAdvancedUnlockedMessageView("") - - biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo -> - // Set listener to open the biometric dialog and save credential - advancedUnlockInfoView?.setIconViewClickListener { _ -> - openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo) - } - } - } - - private fun initDecryptData() { - showFingerPrintViews(true) - setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database) - setAdvancedUnlockedMessageView("") - - if (biometricUnlockDatabaseHelper != null) { - cipherDatabaseAction.getCipherDatabase(databaseFileUri) { - - it?.specParameters?.let { specs -> - biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo -> - - // Set listener to open the biometric dialog and check credential - advancedUnlockInfoView?.setIconViewClickListener { _ -> - openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo) - } - - // Auto open the biometric prompt - if (isBiometricPromptAutoOpenEnable) { - isBiometricPromptAutoOpenEnable = false - openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo) - } - } - } - } - } - } - - @Synchronized - fun initAdvancedUnlockMode() { - mAllowAdvancedUnlockMenu = false - when (biometricMode) { - Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable() - Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired() - Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured() - Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable() - Mode.WAIT_CREDENTIAL -> initWaitData() - Mode.STORE_CREDENTIAL -> initEncryptData() - Mode.EXTRACT_CREDENTIAL -> initDecryptData() - } - - invalidateBiometricMenu() - } - - private fun invalidateBiometricMenu() { - // Show fingerprint key deletion - if (!mAddBiometricMenuInProgress) { - mAddBiometricMenuInProgress = true - cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher -> - mAllowAdvancedUnlockMenu = containsCipher - && (biometricMode != Mode.BIOMETRIC_UNAVAILABLE - && biometricMode != Mode.KEY_MANAGER_UNAVAILABLE) - mAddBiometricMenuInProgress = false - context.invalidateOptionsMenu() - } - } - } - - fun destroy() { - // Close the biometric prompt - allowOpenBiometricPrompt = false - biometricUnlockDatabaseHelper?.closeBiometricPrompt() - // Restore the checked listener - checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener) - } - - fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) { - if (mAllowAdvancedUnlockMenu) - menuInflater.inflate(R.menu.advanced_unlock, menu) - } - - fun deleteEntryKey() { - allowOpenBiometricPrompt = false - advancedUnlockInfoView?.setIconViewClickListener(false, null) - biometricUnlockDatabaseHelper?.closeBiometricPrompt() - biometricUnlockDatabaseHelper?.deleteEntryKey() - cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) { - checkBiometricAvailability() - } - } - - override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) { - loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec) - } - - override fun handleDecryptedResult(decryptedValue: String) { - // Load database directly with password retrieve - loadDatabaseAfterRetrieveCredentials.invoke(decryptedValue) - } - - override fun onInvalidKeyException(e: Exception) { - setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key) - } - - override fun onBiometricException(e: Exception) { - val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: "" - setAdvancedUnlockedMessageView(errorMessage) - } - - private fun showFingerPrintViews(show: Boolean) { - context.runOnUiThread { - advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE - } - } - - private fun setAdvancedUnlockedTitleView(textId: Int) { - context.runOnUiThread { - advancedUnlockInfoView?.setTitle(textId) - } - } - - private fun setAdvancedUnlockedMessageView(textId: Int) { - context.runOnUiThread { - advancedUnlockInfoView?.setMessage(textId) - } - } - - private fun setAdvancedUnlockedMessageView(text: CharSequence) { - context.runOnUiThread { - advancedUnlockInfoView?.message = text - } - } - - enum class Mode { - BIOMETRIC_UNAVAILABLE, - BIOMETRIC_SECURITY_UPDATE_REQUIRED, - BIOMETRIC_NOT_CONFIGURED, - KEY_MANAGER_UNAVAILABLE, - WAIT_CREDENTIAL, - STORE_CREDENTIAL, - EXTRACT_CREDENTIAL - } - - companion object { - - private val TAG = AdvancedUnlockedManager::class.java.name - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/StreamCipherFactory.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/StreamCipherFactory.kt index d36dcd086..b957a9077 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/StreamCipherFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/StreamCipherFactory.kt @@ -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") } } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt index b659dea42..7708e45e3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Kdf.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.crypto.keyDerivation import android.content.res.Resources +import androidx.annotation.StringRes import com.kunzisoft.keepass.R import com.kunzisoft.keepass.stream.bytes16ToUuid import com.kunzisoft.keepass.utils.UnsignedInt @@ -27,7 +28,11 @@ import java.io.IOException import java.security.SecureRandom import java.util.* -class Argon2Kdf internal constructor() : KdfEngine() { +class Argon2Kdf(private val type: Type) : KdfEngine() { + + init { + uuid = type.CIPHER_UUID + } override val defaultParameters: KdfParameters get() { @@ -45,12 +50,8 @@ class Argon2Kdf internal constructor() : KdfEngine() { override val defaultKeyRounds: Long get() = DEFAULT_ITERATIONS - init { - uuid = CIPHER_UUID - } - override fun getName(resources: Resources): String { - return resources.getString(R.string.kdf_Argon2) + return resources.getString(type.nameId) } @Throws(IOException::class) @@ -72,7 +73,9 @@ class Argon2Kdf internal constructor() : KdfEngine() { val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY) val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA) - return Argon2Native.transformKey(masterKey, + return Argon2Native.transformKey( + type, + masterKey, salt, parallelism, memory, @@ -141,9 +144,8 @@ class Argon2Kdf internal constructor() : KdfEngine() { override val maxParallelism: Long get() = MAX_PARALLELISM - companion object { - - val CIPHER_UUID: UUID = bytes16ToUuid( + enum class Type(val CIPHER_UUID: UUID, @StringRes val nameId: Int) { + ARGON2_D(bytes16ToUuid( byteArrayOf(0xEF.toByte(), 0x63.toByte(), 0x6D.toByte(), @@ -159,7 +161,27 @@ class Argon2Kdf internal constructor() : KdfEngine() { 0x03.toByte(), 0xE3.toByte(), 0x0A.toByte(), - 0x0C.toByte())) + 0x0C.toByte())), R.string.kdf_Argon2d), + ARGON2_ID(bytes16ToUuid( + byteArrayOf(0x9E.toByte(), + 0x29.toByte(), + 0x8B.toByte(), + 0x19.toByte(), + 0x56.toByte(), + 0xDB.toByte(), + 0x47.toByte(), + 0x73.toByte(), + 0xB2.toByte(), + 0x3D.toByte(), + 0xFC.toByte(), + 0x3E.toByte(), + 0xC6.toByte(), + 0xF0.toByte(), + 0xA1.toByte(), + 0xE6.toByte())), R.string.kdf_Argon2id); + } + + companion object { private const val PARAM_SALT = "S" // byte[] private const val PARAM_PARALLELISM = "P" // UInt32 diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Native.java b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Native.java index 144479df9..e961d6e81 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Native.java +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/Argon2Native.java @@ -26,12 +26,29 @@ import java.io.IOException; public class Argon2Native { - public static byte[] transformKey(byte[] password, byte[] salt, UnsignedInt parallelism, + enum CType { + ARGON2_D(0), + ARGON2_I(1), + ARGON2_ID(2); + + int cValue = 0; + + CType(int i) { + cValue = i; + } + } + + public static byte[] transformKey(Argon2Kdf.Type type, byte[] password, byte[] salt, UnsignedInt parallelism, UnsignedInt memory, UnsignedInt iterations, byte[] secretKey, byte[] associatedData, UnsignedInt version) throws IOException { NativeLib.INSTANCE.init(); + CType cType = CType.ARGON2_D; + if (type.equals(Argon2Kdf.Type.ARGON2_ID)) + cType = CType.ARGON2_ID; + return nTransformMasterKey( + cType.cValue, password, salt, parallelism.toKotlinInt(), @@ -42,7 +59,7 @@ public class Argon2Native { version.toKotlinInt()); } - private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism, + private static native byte[] nTransformMasterKey(int type, byte[] password, byte[] salt, int parallelism, int memory, int iterations, byte[] secretKey, byte[] associatedData, int version) throws IOException; } diff --git a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt index 7e70becde..8fb3e7de0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/crypto/keyDerivation/KdfFactory.kt @@ -21,5 +21,6 @@ package com.kunzisoft.keepass.crypto.keyDerivation object KdfFactory { var aesKdf = AesKdf() - var argon2Kdf = Argon2Kdf() + var argon2dKdf = Argon2Kdf(Argon2Kdf.Type.ARGON2_D) + var argon2idKdf = Argon2Kdf(Argon2Kdf.Type.ARGON2_ID) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index c21307375..3bb1ed0f7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -24,39 +24,26 @@ import android.net.Uri import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil open class AssignPasswordInDatabaseRunnable ( context: Context, database: Database, protected val mDatabaseUri: Uri, - withMasterPassword: Boolean, - masterPassword: String?, - withKeyFile: Boolean, - keyFile: Uri?) + protected val mMainCredential: MainCredential) : SaveDatabaseRunnable(context, database, true) { - private var mMasterPassword: String? = null - protected var mKeyFileUri: Uri? = null - private var mBackupKey: ByteArray? = null - init { - if (withMasterPassword) - this.mMasterPassword = masterPassword - if (withKeyFile) - this.mKeyFileUri = keyFile - } - override fun onStartRun() { // Set key try { - // TODO move master key methods mBackupKey = ByteArray(database.masterKey.size) System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) - val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri) - database.retrieveMasterKey(mMasterPassword, uriInputStream) + val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri) + database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream) } catch (e: Exception) { erase(mBackupKey) setError(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index 7b7f90d0b..0b55a1bb7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -24,21 +24,18 @@ import android.net.Uri import android.util.Log import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.closeDatabase class CreateDatabaseRunnable(context: Context, private val mDatabase: Database, databaseUri: Uri, private val databaseName: String, private val rootName: String, - withMasterPassword: Boolean, - masterPassword: String?, - withKeyFile: Boolean, - keyFile: Uri?, + mainCredential: MainCredential, private val createDatabaseResult: ((Result) -> Unit)?) - : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) { + : AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { override fun onStartRun() { try { @@ -47,7 +44,7 @@ class CreateDatabaseRunnable(context: Context, createData(mDatabaseUri, databaseName, rootName) } } catch (e: Exception) { - mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) setError(e) } @@ -62,7 +59,7 @@ class CreateDatabaseRunnable(context: Context, if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context.applicationContext) .addOrUpdateDatabaseUri(mDatabaseUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null) + if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) } // Register the current time to init the lock timer diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index 5f9d0ce04..5fba6d91a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -25,19 +25,17 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.closeDatabase class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, private val mUri: Uri, - private val mPass: String?, - private val mKey: Uri?, + private val mMainCredential: MainCredential, private val mReadonly: Boolean, private val mCipherEntity: CipherDatabaseEntity?, private val mFixDuplicateUUID: Boolean, @@ -47,21 +45,19 @@ class LoadDatabaseRunnable(private val context: Context, override fun onStartRun() { // Clear before we load - mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) } override fun onActionRun() { try { - mDatabase.loadData(mUri, mPass, mKey, + mDatabase.loadData(mUri, + mMainCredential, mReadonly, context.contentResolver, UriUtil.getBinaryDir(context), mFixDuplicateUUID, progressTaskUpdater) } - catch (e: DuplicateUuidDatabaseException) { - setError(e) - } catch (e: LoadDatabaseException) { setError(e) } @@ -71,7 +67,7 @@ class LoadDatabaseRunnable(private val context: Context, if (PreferencesUtil.rememberDatabaseLocations(context)) { FileDatabaseHistoryAction.getInstance(context) .addOrUpdateDatabaseUri(mUri, - if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null) + if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) } // Register the biometric @@ -83,7 +79,7 @@ class LoadDatabaseRunnable(private val context: Context, // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(context) } else { - mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt index 279b40e27..2c68513f9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt @@ -26,6 +26,8 @@ import android.net.Uri import android.os.Bundle import android.os.IBinder import androidx.fragment.app.FragmentActivity +import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment +import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.element.Entry @@ -35,34 +37,37 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.model.SnapFileDatabaseInfo +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG @@ -84,6 +89,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { private var serviceConnection: ServiceConnection? = null private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null + private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) { @@ -101,6 +107,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { } } + private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener { + override fun validateDatabaseChanged() { + mBinder?.getService()?.saveDatabaseInfo() + } + } + + private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener { + override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo, + newDatabaseInfo: SnapFileDatabaseInfo) { + if (databaseChangedDialogFragment == null) { + databaseChangedDialogFragment = activity.supportFragmentManager + .findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment? + databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener + } + if (progressTaskDialogFragment == null) { + databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo) + databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener + databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG) + } + } + } + private fun startDialog(titleId: Int? = null, messageId: Int? = null, warningId: Int? = null) { @@ -140,11 +168,14 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { addActionTaskListener(actionTaskListener) + addDatabaseFileInfoListener(databaseInfoListener) getService().checkAction() + getService().checkDatabaseInfo() } } override fun onServiceDisconnected(name: ComponentName?) { + mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) mBinder?.removeActionTaskListener(actionTaskListener) mBinder = null } @@ -206,6 +237,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { fun unregisterProgressTask() { stopDialog() + mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) mBinder?.removeActionTaskListener(actionTaskListener) mBinder = null @@ -233,30 +265,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { */ fun startDatabaseCreate(databaseUri: Uri, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + mainCredential: MainCredential) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) - putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) - putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) - putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked) - putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) } , ACTION_DATABASE_CREATE_TASK) } fun startDatabaseLoad(databaseUri: Uri, - masterPassword: String?, - keyFile: Uri?, + mainCredential: MainCredential, readOnly: Boolean, cipherEntity: CipherDatabaseEntity?, fixDuplicateUuid: Boolean) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) - putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) - putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly) putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity) putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) @@ -264,18 +288,19 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { , ACTION_DATABASE_LOAD_TASK) } + fun startDatabaseReload(fixDuplicateUuid: Boolean) { + start(Bundle().apply { + putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) + } + , ACTION_DATABASE_RELOAD_TASK) + } + fun startDatabaseAssignPassword(databaseUri: Uri, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + mainCredential: MainCredential) { start(Bundle().apply { putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) - putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked) - putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword) - putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked) - putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile) + putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) } , ACTION_DATABASE_ASSIGN_PASSWORD_TASK) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt new file mode 100644 index 000000000..fb78c1dca --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.database.action + +import android.content.Context +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.tasks.ProgressTaskUpdater +import com.kunzisoft.keepass.utils.UriUtil + +class ReloadDatabaseRunnable(private val context: Context, + private val mDatabase: Database, + private val progressTaskUpdater: ProgressTaskUpdater?, + private val mLoadDatabaseResult: ((Result) -> Unit)?) + : ActionRunnable() { + + override fun onStartRun() { + // Clear before we load + mDatabase.clear(UriUtil.getBinaryDir(context)) + } + + override fun onActionRun() { + try { + mDatabase.reloadData(context.contentResolver, + UriUtil.getBinaryDir(context), + progressTaskUpdater) + } catch (e: LoadDatabaseException) { + setError(e) + } + + if (result.isSuccess) { + // Register the current time to init the lock timer + PreferencesUtil.saveCurrentTime(context) + } else { + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) + } + } + + override fun onFinishRun() { + mLoadDatabaseResult?.invoke(result) + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 444b86ba6..3fea65521 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -31,10 +31,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm -import com.kunzisoft.keepass.database.exception.DatabaseOutputException -import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException -import com.kunzisoft.keepass.database.exception.LoadDatabaseException -import com.kunzisoft.keepass.database.exception.SignatureDatabaseException +import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB @@ -44,6 +41,7 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.icons.IconDrawableFactory +import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.stream.readBytes4ToUInt import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.SingletonHolder @@ -330,29 +328,11 @@ class Database { } @Throws(LoadDatabaseException::class) - fun loadData(uri: Uri, password: String?, keyfile: Uri?, - readOnly: Boolean, - contentResolver: ContentResolver, - cacheDirectory: File, - fixDuplicateUUID: Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { - - this.fileUri = uri - isReadOnly = readOnly - if (uri.scheme == "file") { - val file = File(uri.path!!) - isReadOnly = !file.canWrite() - } - - // Pass KeyFile Uri as InputStreams + private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, + openDatabaseKDB: (InputStream) -> DatabaseKDB, + openDatabaseKDBX: (InputStream) -> DatabaseKDBX) { var databaseInputStream: InputStream? = null - var keyFileInputStream: InputStream? = null try { - // Get keyFile inputStream - keyfile?.let { - keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) - } - // Load Data, pass Uris as InputStreams val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) ?: throw IOException("Database input stream cannot be retrieve") @@ -374,22 +354,10 @@ class Database { when { // Header of database KDB - DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB( - cacheDirectory, - fixDuplicateUUID) - .openDatabase(databaseInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream)) // Header of database KDBX - DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX( - cacheDirectory, - fixDuplicateUUID) - .openDatabase(databaseInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream)) // Header not recognized else -> throw SignatureDatabaseException() @@ -397,14 +365,102 @@ class Database { this.mSearchHelper = SearchHelper() loaded = true - } catch (e: LoadDatabaseException) { throw e } catch (e: Exception) { + throw LoadDatabaseException(e) + } finally { + databaseInputStream?.close() + } + } + + @Throws(LoadDatabaseException::class) + fun loadData(uri: Uri, + mainCredential: MainCredential, + readOnly: Boolean, + contentResolver: ContentResolver, + cacheDirectory: File, + fixDuplicateUUID: Boolean, + progressTaskUpdater: ProgressTaskUpdater?) { + + // Save database URI + this.fileUri = uri + + // Check if the file is writable + this.isReadOnly = readOnly + + // Pass KeyFile Uri as InputStreams + var keyFileInputStream: InputStream? = null + try { + // Get keyFile inputStream + mainCredential.keyFileUri?.let { keyFile -> + keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile) + } + + // Read database stream for the first time + readDatabaseStream(contentResolver, uri, + { databaseInputStream -> + DatabaseInputKDB(cacheDirectory) + .openDatabase(databaseInputStream, + mainCredential.masterPassword, + keyFileInputStream, + progressTaskUpdater, + fixDuplicateUUID) + }, + { databaseInputStream -> + DatabaseInputKDBX(cacheDirectory) + .openDatabase(databaseInputStream, + mainCredential.masterPassword, + keyFileInputStream, + progressTaskUpdater, + fixDuplicateUUID) + } + ) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Unable to load keyfile", e) throw FileNotFoundDatabaseException() + } catch (e: LoadDatabaseException) { + throw e + } catch (e: Exception) { + throw LoadDatabaseException(e) } finally { keyFileInputStream?.close() - databaseInputStream?.close() + } + } + + @Throws(LoadDatabaseException::class) + fun reloadData(contentResolver: ContentResolver, + cacheDirectory: File, + progressTaskUpdater: ProgressTaskUpdater?) { + + // Retrieve the stream from the old database URI + try { + fileUri?.let { oldDatabaseUri -> + readDatabaseStream(contentResolver, oldDatabaseUri, + { databaseInputStream -> + DatabaseInputKDB(cacheDirectory) + .openDatabase(databaseInputStream, + masterKey, + progressTaskUpdater) + }, + { databaseInputStream -> + DatabaseInputKDBX(cacheDirectory) + .openDatabase(databaseInputStream, + masterKey, + progressTaskUpdater) + } + ) + } ?: run { + Log.e(TAG, "Database URI is null, database cannot be reloaded") + throw IODatabaseException() + } + } catch (e: FileNotFoundException) { + Log.e(TAG, "Unable to load keyfile", e) + throw FileNotFoundDatabaseException() + } catch (e: LoadDatabaseException) { + throw e + } catch (e: Exception) { + throw LoadDatabaseException(e) } } @@ -426,7 +482,7 @@ class Database { max: Int = Integer.MAX_VALUE): Group? { return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchInfoString, SearchParameters().apply { - searchInTitles = false + searchInTitles = true searchInUserNames = false searchInPasswords = false searchInUrls = true @@ -531,7 +587,7 @@ class Database { this.fileUri = uri } - fun closeAndClear(filesDirectory: File? = null) { + fun clear(filesDirectory: File? = null) { drawFactory.clearCache() // Delete the cache of the database if present mDatabaseKDB?.clearCache() @@ -544,7 +600,10 @@ class Database { } catch (e: Exception) { Log.e(TAG, "Unable to clear the directory cache.", e) } + } + fun clearAndClose(filesDirectory: File? = null) { + clear(filesDirectory) this.mDatabaseKDB = null this.mDatabaseKDBX = null this.fileUri = null @@ -562,7 +621,9 @@ class Database { } } - fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { + fun validatePasswordEncoding(mainCredential: MainCredential): Boolean { + val password = mainCredential.masterPassword + val containsKeyFile = mainCredential.keyFileUri != null return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile) ?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile) ?: false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt index 7733af0a7..b985fbe38 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Entry.kt @@ -426,6 +426,8 @@ class Entry : Node, EntryVersionedInterface { 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 { 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 { database?.binaryPool?.let { binaryPool -> addAttachments(binaryPool, newEntryInfo.attachments) } - // Update date time - lastAccessTime = DateInstant() - lastModificationTime = DateInstant() database?.stopManageEntry(this) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt index 3f1461d48..8678e84ce 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt @@ -163,10 +163,6 @@ class DatabaseKDB : DatabaseVersioned() { finalKey = messageDigest.digest() } - override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { - return null - } - override fun createGroup(): GroupKDB { return GroupKDB() } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index 8c117931b..00c44ccdc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -43,10 +43,12 @@ import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars +import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.VariantDictionary +import org.apache.commons.codec.binary.Hex import org.w3c.dom.Node -import org.w3c.dom.Text import java.io.File import java.io.IOException import java.io.InputStream @@ -113,7 +115,8 @@ class DatabaseKDBX : DatabaseVersioned { init { kdfList.add(KdfFactory.aesKdf) - kdfList.add(KdfFactory.argon2Kdf) + kdfList.add(KdfFactory.argon2dKdf) + kdfList.add(KdfFactory.argon2idKdf) } constructor() @@ -123,6 +126,7 @@ class DatabaseKDBX : DatabaseVersioned { */ constructor(databaseName: String, rootName: String) { name = databaseName + kdbxVersion = FILE_VERSION_32_3 val group = createGroup().apply { title = rootName icon = iconFactory.folderIcon @@ -179,7 +183,8 @@ class DatabaseKDBX : DatabaseVersioned { when (oldCompression) { CompressionAlgorithm.None -> { when (newCompression) { - CompressionAlgorithm.None -> {} + CompressionAlgorithm.None -> { + } CompressionAlgorithm.GZip -> { // Only in databaseV3.1, in databaseV4 the header is zipped during the save if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) { @@ -197,7 +202,8 @@ class DatabaseKDBX : DatabaseVersioned { CompressionAlgorithm.None -> { decompressAllBinaries() } - CompressionAlgorithm.GZip -> {} + CompressionAlgorithm.GZip -> { + } } } } @@ -377,36 +383,82 @@ class DatabaseKDBX : DatabaseVersioned { 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 + // 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) + // + 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) + // + 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") + } + } + } + } + } + } + // + 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) + // + if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) { + var hashString : String? = null + if (keyChildNode.hasAttributes()) { + val dataNodeAttributes = keyChildNode.attributes + hashString = dataNodeAttributes + .getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue + } + val dataChildNodes = keyChildNode.childNodes + for (dataChildPosition in 0 until dataChildNodes.length) { + val dataChildNode = dataChildNodes.item(dataChildPosition) + if (dataChildNode.nodeType == Node.TEXT_NODE) { + val dataString = dataChildNode.textContent.removeSpaceChars() + when (xmlKeyFileVersion) { + 1F -> { + // No hash in KeyFile XML version 1 + return Base64.decode(dataString, BASE_64_FLAG) + } + 2F -> { + return if (hashString != null + && checkKeyFileHash(dataString, hashString)) { + Log.i(TAG, "Successful key file hash check.") + Hex.decodeHex(dataString.toCharArray()) + } else { + Log.e(TAG, "Unable to check the hash of the key file.") + null + } + } + } } } } @@ -416,10 +468,26 @@ class DatabaseKDBX : DatabaseVersioned { } catch (e: Exception) { return null } - return null } + private fun checkKeyFileHash(data: String, hash: String): Boolean { + val digest: MessageDigest? + var success = false + try { + digest = MessageDigest.getInstance("SHA-256") + digest?.reset() + // hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key. + val dataDigest = digest.digest(Hex.decodeHex(data.toCharArray())) + .copyOfRange(0, 4) + .toHexString() + success = dataDigest == hash + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return success + } + override fun newGroupId(): NodeIdUUID { var newId: NodeIdUUID do { @@ -633,11 +701,12 @@ class DatabaseKDBX : DatabaseVersioned { 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 diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt index dd2a0ea35..660434e5f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt @@ -27,7 +27,11 @@ import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException -import java.io.* +import org.apache.commons.codec.binary.Hex +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.io.UnsupportedEncodingException import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* @@ -124,42 +128,35 @@ abstract class DatabaseVersioned< @Throws(IOException::class) protected fun getFileKey(keyInputStream: InputStream): ByteArray { - val keyByteArrayOutputStream = ByteArrayOutputStream() - keyInputStream.copyTo(keyByteArrayOutputStream) - val keyData = keyByteArrayOutputStream.toByteArray() + val keyData = keyInputStream.readBytes() - val keyByteArrayInputStream = ByteArrayInputStream(keyData) - val key = loadXmlKeyFile(keyByteArrayInputStream) - if (key != null) { - return key + // Check XML key file + val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) + if (xmlKeyByteArray != null) { + return xmlKeyByteArray } - when (keyData.size.toLong()) { - 32L -> return keyData - 64L -> try { - return hexStringToByteArray(String(keyData)) - } catch (e: IndexOutOfBoundsException) { + // Check 32 bytes key file + when (keyData.size) { + 32 -> return keyData + 64 -> try { + return Hex.decodeHex(String(keyData).toCharArray()) + } catch (ignoredException: Exception) { // Key is not base 64, treat it as binary data } } - val messageDigest: MessageDigest + // Hash file as binary data try { - messageDigest = MessageDigest.getInstance("SHA-256") + return MessageDigest.getInstance("SHA-256").digest(keyData) } catch (e: NoSuchAlgorithmException) { throw IOException("SHA-256 not supported") } - - try { - messageDigest.update(keyData) - } catch (e: Exception) { - println(e.toString()) - } - - return messageDigest.digest() } - protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? + protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { + return null + } open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { if (password == null && !containsKeyFile) @@ -391,16 +388,5 @@ abstract class DatabaseVersioned< private const val TAG = "DatabaseVersioned" val UUID_ZERO = UUID(0, 0) - - fun hexStringToByteArray(s: String): ByteArray { - val len = s.length - val data = ByteArray(len / 2) - var i = 0 - while (i < len) { - data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() - i += 2 - } - return data - } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt index 594154606..630fbd384 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/exception/DatabaseException.kt @@ -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) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt index 8da334872..ad819cde5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt @@ -41,6 +41,13 @@ abstract class DatabaseInput> 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 } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt index dd5ac5b6b..f2b67e788 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt @@ -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(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 { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt index e9e3fa0b8..14276b2a9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt @@ -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(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 } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutput.kt deleted file mode 100644 index 4cb13458d..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutput.kt +++ /dev/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 . - * - */ -package com.kunzisoft.keepass.database.file.output - -open class DatabaseHeaderOutput { - var hashOfHeader: ByteArray? = null - protected set -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt index bd18d8207..de76b662d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt @@ -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 diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseInnerHeaderOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseInnerHeaderOutputKDBX.kt deleted file mode 100644 index fb3666e90..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseInnerHeaderOutputKDBX.kt +++ /dev/null @@ -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 . - * - */ -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) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt index 4c0951393..4d456e538 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt @@ -26,7 +26,7 @@ import java.io.OutputStream import java.security.NoSuchAlgorithmException import java.security.SecureRandom -abstract class DatabaseOutput
protected constructor(protected var mOS: OutputStream) { +abstract class DatabaseOutput
protected constructor(protected var mOutputStream: OutputStream) { @Throws(DatabaseOutputException::class) protected open fun setIVs(header: Header): SecureRandom { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt index f71345bf8..417865c21 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt @@ -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() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt index 422a0fb49..aa3f40ee4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt @@ -38,7 +38,6 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface -import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.exception.DatabaseOutputException @@ -47,6 +46,7 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseKDBXXML import com.kunzisoft.keepass.database.file.DateKDBXUtil import com.kunzisoft.keepass.stream.* +import com.kunzisoft.keepass.utils.UnsignedInt import org.bouncycastle.crypto.StreamCipher import org.joda.time.DateTime import org.xmlpull.v1.XmlSerializer @@ -58,6 +58,7 @@ import java.util.* import java.util.zip.GZIPOutputStream import javax.crypto.Cipher import javax.crypto.CipherOutputStream +import kotlin.experimental.or class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, @@ -81,20 +82,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, throw DatabaseOutputException("No such cipher", e) } - header = outputHeader(mOS) + header = outputHeader(mOutputStream) val osPlain: OutputStream osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { - val cos = attachStreamEncryptor(header!!, mOS) + val cos = attachStreamEncryptor(header!!, mOutputStream) cos.write(header!!.streamStartBytes) HashedBlockOutputStream(cos) } else { - mOS.write(hashOfHeader!!) - mOS.write(headerHmac!!) + mOutputStream.write(hashOfHeader!!) + mOutputStream.write(headerHmac!!) - - attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey!!)) + attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!)) } val osXml: OutputStream @@ -105,8 +105,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { - val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml) - ihOut.output() + outputInnerHeader(mDatabaseKDBX, header!!, osXml) } outputDatabase(osXml) @@ -122,6 +121,49 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } } + @Throws(IOException::class) + private fun outputInnerHeader(database: DatabaseKDBX, + header: DatabaseHeaderKDBX, + outputStream: OutputStream) { + val dataOutputStream = LittleEndianDataOutputStream(outputStream) + + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID) + dataOutputStream.writeInt(4) + if (header.innerRandomStream == null) + throw IOException("Can't write innerRandomStream") + dataOutputStream.writeUInt(header.innerRandomStream!!.id) + + val streamKeySize = header.innerRandomStreamKey.size + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey) + dataOutputStream.writeInt(streamKeySize) + dataOutputStream.write(header.innerRandomStreamKey) + + database.binaryPool.doForEachOrderedBinary { _, keyBinary -> + val protectedBinary = keyBinary.binary + // Force decompression to add binary in header + protectedBinary.decompress() + // Write type binary + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) + // Write size + dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1)) + // Write protected flag + var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None + if (protectedBinary.isProtected) { + flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected + } + dataOutputStream.writeByte(flag) + + protectedBinary.getInputDataStream().use { inputStream -> + inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + dataOutputStream.write(buffer) + } + } + } + + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader) + dataOutputStream.writeInt(0) + } + @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun outputDatabase(outputStream: OutputStream) { @@ -282,9 +324,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } random.nextBytes(header.innerRandomStreamKey) - randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) - if (randomStream == null) { - throw DatabaseOutputException("Invalid random cipher") + try { + randomStream = StreamCipherFactory.getInstance(header.innerRandomStream, header.innerRandomStreamKey) + } catch (e: Exception) { + throw DatabaseOutputException(e) } if (header.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { @@ -420,41 +463,56 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, writeObject(name, String(Base64.encode(data, BASE_64_FLAG))) } + /* + // Normally used by a single entry but obsolete because binaries are in meta tag with kdbx3.1- + // or in file header with kdbx4 + // binary.isProtected attribute is not used to create the XML @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - private fun writeBinary(binary : BinaryAttachment) { - val binaryLength = binary.length() - if (binaryLength > 0) { + private fun writeEntryBinary(binary : BinaryAttachment) { + if (binary.length() > 0) { if (binary.isProtected) { xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) - - binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer -> - val encoded = ByteArray(buffer.size) - randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0) - val charArray = String(Base64.encode(encoded, BASE_64_FLAG)).toCharArray() - xml.text(charArray, 0, charArray.size) + binary.getInputDataStream().use { inputStream -> + inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + val encoded = ByteArray(buffer.size) + randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0) + xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) + } } } else { - if (binary.isCompressed) { - xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) - } // Write the XML - binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer -> - val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray() - xml.text(charArray, 0, charArray.size) + binary.getInputDataStream().use { inputStream -> + inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) + } } } } } + */ + // Only uses with kdbx3.1 to write binaries in meta tag + // With kdbx4, don't use this method because binaries are in header file @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun writeMetaBinaries() { xml.startTag(null, DatabaseKDBXXML.ElemBinaries) - // Use indexes because necessarily in DatabaseV4 (binary header ref is the order) + // Use indexes because necessarily (binary header ref is the order) mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary -> xml.startTag(null, DatabaseKDBXXML.ElemBinary) xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString()) - writeBinary(keyBinary.binary) + val binary = keyBinary.binary + if (binary.length() > 0) { + if (binary.isCompressed) { + xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) + } + // Write the XML + binary.getInputDataStream().use { inputStream -> + inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + xml.text(String(Base64.encode(buffer, BASE_64_FLAG))) + } + } + } xml.endTag(null, DatabaseKDBXXML.ElemBinary) } @@ -523,17 +581,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, if (protect) { xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) - - val data = value.toString().toByteArray(charset("UTF-8")) - val valLength = data.size - - if (valLength > 0) { - val encoded = ByteArray(valLength) - randomStream!!.processBytes(data, 0, valLength, encoded, 0) + val data = value.toString().toByteArray() + val dataLength = data.size + if (data.isNotEmpty()) { + val encoded = ByteArray(dataLength) + randomStream!!.processBytes(data, 0, dataLength, encoded, 0) xml.text(String(Base64.encode(encoded, BASE_64_FLAG))) } } else { - xml.text(safeXmlString(value.toString())) + xml.text(value.toString()) } xml.endTag(null, DatabaseKDBXXML.ElemValue) @@ -662,17 +718,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, if (text.isEmpty()) { return text } - val stringBuilder = StringBuilder() - var ch: Char + var character: Char for (element in text) { - ch = element + character = element + val hexChar = character.toInt() if ( - ch.toInt() in 0x20..0xD7FF || - ch.toInt() == 0x9 || ch.toInt() == 0xA || ch.toInt() == 0xD || - ch.toInt() in 0xE000..0xFFFD + hexChar in 0x20..0xD7FF || + hexChar == 0x9 || + hexChar == 0xA || + hexChar == 0xD || + hexChar in 0xE000..0xFFFD ) { - stringBuilder.append(ch) + stringBuilder.append(character) } } return stringBuilder.toString() diff --git a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt index e74303fbf..d3d3a044e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/PasswordActivityEducation.kt @@ -86,8 +86,8 @@ class PasswordActivityEducation(activity: Activity) onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean { return checkAndPerformedEducation(!isEducationBiometricPerformed(activity), TapTarget.forView(educationView, - activity.getString(R.string.education_biometric_title), - activity.getString(R.string.education_biometric_summary)) + activity.getString(R.string.education_advanced_unlock_title), + activity.getString(R.string.education_advanced_unlock_summary)) .textColorInt(Color.WHITE) .tintTarget(false) .cancelable(true), diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt index 691273b25..add39ee52 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt @@ -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 +} diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt index 0af4a798a..5dc9730be 100644 --- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt +++ b/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikIME.kt @@ -41,7 +41,8 @@ import com.kunzisoft.keepass.adapters.FieldsAdapter import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.Field -import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService +import com.kunzisoft.keepass.services.KeyboardEntryNotificationService +import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.* @@ -243,6 +244,14 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { if (entryInfoKey != null) { currentInputConnection.commitText(entryInfoKey!!.password, 1) } + val otpFieldExists = entryInfoKey?.containsCustomField(OTP_TOKEN_FIELD) ?: false + actionGoAutomatically(!otpFieldExists) + } + KEY_OTP -> { + if (entryInfoKey != null) { + currentInputConnection.commitText( + entryInfoKey!!.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1) + } actionGoAutomatically() } KEY_URL -> { @@ -254,7 +263,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { KEY_FIELDS -> { if (entryInfoKey != null) { fieldsAdapter?.apply { - setFields(entryInfoKey!!.customFields) + setFields(entryInfoKey!!.customFields.filter { it.name != OTP_TOKEN_FIELD}) notifyDataSetChanged() } } @@ -272,10 +281,11 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { currentInputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB)) } - private fun actionGoAutomatically() { + private fun actionGoAutomatically(switchToPreviousKeyboardIfAllowed: Boolean = true) { if (PreferencesUtil.isAutoGoActionEnable(this)) { currentInputConnection.performEditorAction(EditorInfo.IME_ACTION_GO) - if (PreferencesUtil.isKeyboardPreviousFillInEnable(this)) { + if (switchToPreviousKeyboardIfAllowed + && PreferencesUtil.isKeyboardPreviousFillInEnable(this)) { switchToPreviousKeyboard() } } @@ -326,6 +336,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener { private const val KEY_ENTRY = 620 private const val KEY_USERNAME = 500 private const val KEY_PASSWORD = 510 + private const val KEY_OTP = 515 private const val KEY_URL = 520 private const val KEY_FIELDS = 530 diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 69c965b2c..73f2ab8f2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -39,6 +39,8 @@ class EntryInfo : Parcelable { var icon: IconImage = IconImageStandard() var username: String = "" var password: String = "" + var creationTime: DateInstant = DateInstant() + var modificationTime: DateInstant = DateInstant() var expires: Boolean = false var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH var url: String = "" @@ -55,6 +57,8 @@ class EntryInfo : Parcelable { icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon username = parcel.readString() ?: username password = parcel.readString() ?: password + creationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: creationTime + modificationTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: modificationTime expires = parcel.readInt() != 0 expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime url = parcel.readString() ?: url @@ -74,6 +78,8 @@ class EntryInfo : Parcelable { parcel.writeParcelable(icon, flags) parcel.writeString(username) parcel.writeString(password) + parcel.writeParcelable(creationTime, flags) + parcel.writeParcelable(modificationTime, flags) parcel.writeInt(if (expires) 1 else 0) parcel.writeParcelable(expiryTime, flags) parcel.writeString(url) @@ -91,13 +97,13 @@ class EntryInfo : Parcelable { return customFields.any { !it.protectedValue.isProtected } } - fun isAutoGeneratedField(field: Field): Boolean { - return field.name == OTP_TOKEN_FIELD + fun containsCustomField(label: String): Boolean { + return customFields.lastOrNull { it.name == label } != null } fun getGeneratedFieldValue(label: String): String { - otpModel?.let { - if (label == OTP_TOKEN_FIELD) { + if (label == OTP_TOKEN_FIELD) { + otpModel?.let { return OtpElement(it).token } } diff --git a/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt new file mode 100644 index 000000000..e5ee12c72 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/MainCredential.kt @@ -0,0 +1,32 @@ +package com.kunzisoft.keepass.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readParcelable(Uri::class.java.classLoader)) { + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(masterPassword) + parcel.writeParcelable(keyFileUri, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MainCredential { + return MainCredential(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt new file mode 100644 index 000000000..6d6bbbbd9 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt @@ -0,0 +1,82 @@ +package com.kunzisoft.keepass.model + +import android.content.Context +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.text.format.Formatter +import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo +import java.text.DateFormat +import java.util.* + +/** + * Utility data class to get FileDatabaseInfo at a `t` time + */ +data class SnapFileDatabaseInfo(var fileUri: Uri?, + var exists: Boolean, + var lastModification: Long?, + var size: Long?): Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readParcelable(Uri::class.java.classLoader), + parcel.readByte() != 0.toByte(), + parcel.readValue(Long::class.java.classLoader) as? Long, + parcel.readValue(Long::class.java.classLoader) as? Long) { + } + + fun toString(context: Context): String { + val lastModificationString = DateFormat.getDateTimeInstance() + .format(Date(lastModification ?: 0)) + return "$lastModificationString, " + + Formatter.formatFileSize(context, size ?: 0) + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(fileUri, flags) + parcel.writeByte(if (exists) 1 else 0) + parcel.writeValue(lastModification) + parcel.writeValue(size) + } + + override fun describeContents(): Int { + return 0 + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SnapFileDatabaseInfo) return false + + if (fileUri != other.fileUri) return false + if (exists != other.exists) return false + if (lastModification != other.lastModification) return false + if (size != other.size) return false + + return true + } + + override fun hashCode(): Int { + var result = fileUri?.hashCode() ?: 0 + result = 31 * result + exists.hashCode() + result = 31 * result + (lastModification?.hashCode() ?: 0) + result = 31 * result + (size?.hashCode() ?: 0) + return result + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SnapFileDatabaseInfo { + return SnapFileDatabaseInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + fun fromFileDatabaseInfo(fileDatabaseInfo: FileDatabaseInfo): SnapFileDatabaseInfo { + return SnapFileDatabaseInfo( + fileDatabaseInfo.fileUri, + fileDatabaseInfo.exists, + fileDatabaseInfo.getLastModification(), + fileDatabaseInfo.getSize()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt index 682f7889e..bddb8a284 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.otp import com.kunzisoft.keepass.model.OtpModel +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Hex @@ -137,7 +138,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) { @Throws(IllegalArgumentException::class) fun setHexSecret(secret: String) { if (secret.isNotEmpty()) - otpModel.secret = Hex.decodeHex(secret) + otpModel.secret = Hex.decodeHex(secret.toCharArray()) else throw IllegalArgumentException() } @@ -150,16 +151,16 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) { @Throws(IllegalArgumentException::class) fun setBase32Secret(secret: String) { - if (isValidBase32(secret)) - otpModel.secret = Base32().decode(replaceBase32Chars(secret).toByteArray()) - else + if (isValidBase32(secret)) { + otpModel.secret = Base32().decode(replaceBase32Chars(secret)) + } else throw IllegalArgumentException() } @Throws(IllegalArgumentException::class) fun setBase64Secret(secret: String) { if (isValidBase64(secret)) - otpModel.secret = Base64().decode(secret.toByteArray()) + otpModel.secret = Base64().decode(secret) else throw IllegalArgumentException() } @@ -208,38 +209,24 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) { fun isValidBase32(secret: String): Boolean { val secretChars = replaceBase32Chars(secret) - return secretChars.isNotEmpty() && checkBase32Secret(secretChars) + return secret.isNotEmpty() + && (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secretChars)) } fun isValidBase64(secret: String): Boolean { // TODO replace base 64 chars - return secret.isNotEmpty() && checkBase64Secret(secret) - } - - fun removeLineChars(parameter: String): String { - return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "") - } - - fun removeSpaceChars(parameter: String): String { - return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "") + return secret.isNotEmpty() + && (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret)) } fun replaceBase32Chars(parameter: String): String { - // Add 'A' at end if not Base32 length - var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH)) + // Add padding '=' at end if not Base32 length + var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars() while (parameterNewSize.length % 8 != 0) { - parameterNewSize += 'A' + parameterNewSize += '=' } return parameterNewSize } - - fun checkBase32Secret(secret: String): Boolean { - return (Pattern.matches("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$", secret)) - } - - fun checkBase64Secret(secret: String): Boolean { - return (Pattern.matches("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", secret)) - } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt index 38106feca..9e382b39a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt @@ -24,9 +24,9 @@ import android.net.Uri import android.util.Log import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.model.Field -import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars -import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars import com.kunzisoft.keepass.otp.TokenCalculator.* +import com.kunzisoft.keepass.utils.StringUtil.removeLineChars +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars import java.util.* import java.util.regex.Pattern @@ -57,13 +57,25 @@ object OtpEntryFields { private const val DIGITS_KEY = "size" private const val STEP_KEY = "step" - // HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#hmacotp) + // HmacOtp KeePass2 values (https://keepass.info/help/base/placeholders.html#otp) private const val HMACOTP_SECRET_FIELD = "HmacOtp-Secret" private const val HMACOTP_SECRET_HEX_FIELD = "HmacOtp-Secret-Hex" private const val HMACOTP_SECRET_BASE32_FIELD = "HmacOtp-Secret-Base32" private const val HMACOTP_SECRET_BASE64_FIELD = "HmacOtp-Secret-Base64" private const val HMACOTP_SECRET_COUNTER_FIELD = "HmacOtp-Counter" + // TimeOtp KeePass2 values + private const val TIMEOTP_SECRET_FIELD = "TimeOtp-Secret" + private const val TIMEOTP_SECRET_HEX_FIELD = "TimeOtp-Secret-Hex" + private const val TIMEOTP_SECRET_BASE32_FIELD = "TimeOtp-Secret-Base32" + private const val TIMEOTP_SECRET_BASE64_FIELD = "TimeOtp-Secret-Base64" + private const val TIMEOTP_LENGTH_FIELD = "TimeOtp-Length" + private const val TIMEOTP_PERIOD_FIELD = "TimeOtp-Period" + private const val TIMEOTP_ALGORITHM_FIELD = "TimeOtp-Algorithm" + private const val TIMEOTP_ALGORITHM_SHA1_VALUE = "HMAC-SHA-1" + private const val TIMEOTP_ALGORITHM_SHA256_VALUE = "HMAC-SHA-256" + private const val TIMEOTP_ALGORITHM_SHA512_VALUE = "HMAC-SHA-512" + // Custom fields (maybe from plugin) private const val TOTP_SEED_FIELD = "TOTP Seed" private const val TOTP_SETTING_FIELD = "TOTP Settings" @@ -85,14 +97,17 @@ object OtpEntryFields { // OTP (HOTP/TOTP) from URL and field from KeePassXC if (parseOTPUri(getField, otpElement)) return otpElement + // TOTP from KeePass 2.47 + if (parseTOTPFromOfficialField(getField, otpElement)) + return otpElement // TOTP from key values (maybe plugin or old KeePassXC) if (parseTOTPKeyValues(getField, otpElement)) return otpElement // TOTP from custom field - if (parseTOTPFromField(getField, otpElement)) + if (parseTOTPFromPluginField(getField, otpElement)) return otpElement // HOTP fields from KeePass 2 - if (parseHOTPFromField(getField, otpElement)) + if (parseHOTPFromOfficialField(getField, otpElement)) return otpElement return null } @@ -126,7 +141,7 @@ object OtpEntryFields { private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { val otpPlainText = getField(OTP_FIELD) if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) { - val uri = Uri.parse(removeSpaceChars(otpPlainText)) + val uri = Uri.parse(otpPlainText.removeSpaceChars()) if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) { Log.e(TAG, "Invalid or missing scheme in uri") @@ -159,16 +174,16 @@ object OtpEntryFields { if (nameParam != null && nameParam.isNotEmpty()) { val userIdArray = nameParam.split(":", "%3A") if (userIdArray.size > 1) { - otpElement.issuer = removeLineChars(userIdArray[0]) - otpElement.name = removeLineChars(userIdArray[1]) + otpElement.issuer = userIdArray[0].removeLineChars() + otpElement.name = userIdArray[1].removeLineChars() } else { - otpElement.name = removeLineChars(nameParam) + otpElement.name = nameParam.removeLineChars() } } val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM) if (issuerParam != null && issuerParam.isNotEmpty()) - otpElement.issuer = removeLineChars(issuerParam) + otpElement.issuer = issuerParam.removeLineChars() val secretParam = uri.getQueryParameter(SECRET_URL_PARAM) if (secretParam != null && secretParam.isNotEmpty()) { @@ -247,8 +262,9 @@ object OtpEntryFields { encodeParameter(username) else encodeParameter(otpElement.name) + val secret = encodeParameter(otpElement.getBase32Secret()) val uriString = StringBuilder("otpauth://$otpAuthority/$issuer%3A$accountName" + - "?$SECRET_URL_PARAM=${otpElement.getBase32Secret()}" + + "?$SECRET_URL_PARAM=${secret}" + "&$counterOrPeriodLabel=$counterOrPeriodValue" + "&$DIGITS_URL_PARAM=${otpElement.digits}" + "&$ISSUER_URL_PARAM=$issuer") @@ -262,7 +278,40 @@ object OtpEntryFields { } private fun encodeParameter(parameter: String): String { - return Uri.encode(OtpElement.removeLineChars(parameter)) + return Uri.encode(parameter.removeLineChars()) + } + + private fun parseTOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + val secretField = getField(TIMEOTP_SECRET_FIELD) + val secretHexField = getField(TIMEOTP_SECRET_HEX_FIELD) + val secretBase32Field = getField(TIMEOTP_SECRET_BASE32_FIELD) + val secretBase64Field = getField(TIMEOTP_SECRET_BASE64_FIELD) + val lengthField = getField(TIMEOTP_LENGTH_FIELD) + val periodField = getField(TIMEOTP_PERIOD_FIELD) + val algorithmField = getField(TIMEOTP_ALGORITHM_FIELD) + try { + when { + secretField != null -> otpElement.setUTF8Secret(secretField) + secretHexField != null -> otpElement.setHexSecret(secretHexField) + secretBase32Field != null -> otpElement.setBase32Secret(secretBase32Field) + secretBase64Field != null -> otpElement.setBase64Secret(secretBase64Field) + lengthField != null -> otpElement.digits = lengthField.toIntOrNull() ?: OTP_DEFAULT_DIGITS + periodField != null -> otpElement.period = periodField.toIntOrNull() ?: TOTP_DEFAULT_PERIOD + algorithmField != null -> otpElement.algorithm = + when (algorithmField.toUpperCase(Locale.ENGLISH)) { + TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1 + TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256 + TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512 + else -> HashAlgorithm.SHA1 + } + else -> return false + } + } catch (exception: Exception) { + return false + } + + otpElement.type = OtpType.TOTP + return true } private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { @@ -290,7 +339,7 @@ object OtpEntryFields { return false } - private fun parseTOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + private fun parseTOTPFromPluginField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { val seedField = getField(TOTP_SEED_FIELD) ?: return false try { otpElement.setBase32Secret(seedField) @@ -305,9 +354,15 @@ object OtpEntryFields { return false } otpElement.period = matcher.group(1)?.toIntOrNull() ?: TOTP_DEFAULT_PERIOD - otpElement.tokenType = matcher.group(2)?.let { - OtpTokenType.getFromString(it) - } ?: OtpTokenType.RFC6238 + matcher.group(2)?.let { secondMatcher -> + try { + otpElement.digits = secondMatcher.toInt() + } catch (e: NumberFormatException) { + otpElement.digits = OTP_DEFAULT_DIGITS + otpElement.tokenType = OtpTokenType.getFromString(secondMatcher) + } + } + } } catch (exception: Exception) { return false @@ -316,7 +371,7 @@ object OtpEntryFields { return true } - private fun parseHOTPFromField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { + private fun parseHOTPFromOfficialField(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { val secretField = getField(HMACOTP_SECRET_FIELD) val secretHexField = getField(HMACOTP_SECRET_HEX_FIELD) val secretBase32Field = getField(HMACOTP_SECRET_BASE32_FIELD) @@ -382,25 +437,43 @@ object OtpEntryFields { val totpSeedField = Field(TOTP_SEED_FIELD) val totpSettingField = Field(TOTP_SETTING_FIELD) val hmacOtpSecretField = Field(HMACOTP_SECRET_FIELD) - val hmacOtpSecretHewField = Field(HMACOTP_SECRET_HEX_FIELD) + val hmacOtpSecretHexField = Field(HMACOTP_SECRET_HEX_FIELD) val hmacOtpSecretBase32Field = Field(HMACOTP_SECRET_BASE32_FIELD) val hmacOtpSecretBase64Field = Field(HMACOTP_SECRET_BASE64_FIELD) val hmacOtpSecretCounterField = Field(HMACOTP_SECRET_COUNTER_FIELD) + val timeOtpSecretField = Field(TIMEOTP_SECRET_FIELD) + val timeOtpSecretHexField = Field(TIMEOTP_SECRET_HEX_FIELD) + val timeOtpSecretBase32Field = Field(TIMEOTP_SECRET_BASE32_FIELD) + val timeOtpSecretBase64Field = Field(TIMEOTP_SECRET_BASE64_FIELD) + val timeOtpLengthField = Field(TIMEOTP_LENGTH_FIELD) + val timeOtpPeriodField = Field(TIMEOTP_PERIOD_FIELD) + val timeOtpAlgorithmField = Field(TIMEOTP_ALGORITHM_FIELD) newCustomFields.remove(otpField) newCustomFields.remove(totpSeedField) newCustomFields.remove(totpSettingField) newCustomFields.remove(hmacOtpSecretField) - newCustomFields.remove(hmacOtpSecretHewField) + newCustomFields.remove(hmacOtpSecretHexField) newCustomFields.remove(hmacOtpSecretBase32Field) newCustomFields.remove(hmacOtpSecretBase64Field) newCustomFields.remove(hmacOtpSecretCounterField) + newCustomFields.remove(timeOtpSecretField) + newCustomFields.remove(timeOtpSecretHexField) + newCustomFields.remove(timeOtpSecretBase32Field) + newCustomFields.remove(timeOtpSecretBase64Field) + newCustomFields.remove(timeOtpLengthField) + newCustomFields.remove(timeOtpPeriodField) + newCustomFields.remove(timeOtpAlgorithmField) // Empty auto generated OTP Token field if (fieldsToParse.contains(otpField) || fieldsToParse.contains(totpSeedField) || fieldsToParse.contains(hmacOtpSecretField) - || fieldsToParse.contains(hmacOtpSecretHewField) + || fieldsToParse.contains(hmacOtpSecretHexField) || fieldsToParse.contains(hmacOtpSecretBase32Field) || fieldsToParse.contains(hmacOtpSecretBase64Field) + || fieldsToParse.contains(timeOtpSecretField) + || fieldsToParse.contains(timeOtpSecretHexField) + || fieldsToParse.contains(timeOtpSecretBase32Field) + || fieldsToParse.contains(timeOtpSecretBase64Field) ) newCustomFields.add(Field(OTP_TOKEN_FIELD)) return newCustomFields diff --git a/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt new file mode 100644 index 000000000..26c56d01f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/services/AdvancedUnlockNotificationService.kt @@ -0,0 +1,143 @@ +package com.kunzisoft.keepass.services + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Binder +import android.os.IBinder +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.timeout.TimeoutHelper +import kotlinx.coroutines.* + +class AdvancedUnlockNotificationService : NotificationService() { + + private lateinit var mTempCipherDao: ArrayList + + private var mActionTaskBinder = AdvancedUnlockBinder() + + private var notificationTimeoutMilliSecs: Long = 0 + private var mTimerJob: Job? = null + + inner class AdvancedUnlockBinder: Binder() { + fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? { + return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()} + } + fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity) { + val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull { it.databaseUri == cipherDatabaseEntity.databaseUri } + cipherDatabaseRetrieve?.replaceContent(cipherDatabaseEntity) + ?: mTempCipherDao.add(cipherDatabaseEntity) + } + fun deleteByDatabaseUri(databaseUri: Uri) { + mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString() }?.let { + mTempCipherDao.remove(it) + } + } + fun deleteAll() { + mTempCipherDao.clear() + } + } + + override val notificationId: Int = 593 + + override fun retrieveChannelId(): String { + return CHANNEL_ADVANCED_UNLOCK_ID + } + + override fun retrieveChannelName(): String { + return getString(R.string.advanced_unlock) + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return mActionTaskBinder + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + val deleteIntent = Intent(this, AdvancedUnlockNotificationService::class.java).apply { + action = ACTION_REMOVE_KEYS + } + val pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this) + val notificationBuilder = buildNewNotification().apply { + setSmallIcon(if (biometricUnlockEnabled) { + R.drawable.notification_ic_fingerprint_unlock_24dp + } else { + R.drawable.notification_ic_device_unlock_24dp + }) + intent?.let { + setContentTitle(getString(R.string.advanced_unlock)) + } + setContentText(getString(R.string.advanced_unlock_tap_delete)) + setContentIntent(pendingDeleteIntent) + // Unfortunately swipe is disabled in lollipop+ + setDeleteIntent(pendingDeleteIntent) + } + + when (intent?.action) { + ACTION_TIMEOUT -> { + notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this) + // Not necessarily a foreground service + if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) { + mTimerJob = CoroutineScope(Dispatchers.Main).launch { + val maxPos = 100 + val posDurationMills = notificationTimeoutMilliSecs / maxPos + for (pos in maxPos downTo 0) { + notificationBuilder.setProgress(maxPos, pos, false) + startForeground(notificationId, notificationBuilder.build()) + delay(posDurationMills) + if (pos <= 0) { + stopSelf() + } + } + notificationManager?.cancel(notificationId) + mTimerJob = null + cancel() + } + } else { + startForeground(notificationId, notificationBuilder.build()) + } + } + ACTION_REMOVE_KEYS -> { + stopSelf() + } + else -> {} + } + + return START_STICKY + } + + override fun onCreate() { + super.onCreate() + mTempCipherDao = ArrayList() + } + + override fun onDestroy() { + mTempCipherDao.clear() + mTimerJob?.cancel() + super.onDestroy() + } + + companion object { + private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock" + + private const val ACTION_TIMEOUT = "ACTION_TIMEOUT" + private const val ACTION_REMOVE_KEYS = "ACTION_REMOVE_KEYS" + + fun startServiceForTimeout(context: Context) { + if (PreferencesUtil.isTempAdvancedUnlockEnable(context)) { + context.startService(Intent(context, AdvancedUnlockNotificationService::class.java).apply { + action = ACTION_TIMEOUT + }) + } + } + + fun stopService(context: Context) { + context.stopService(Intent(context, AdvancedUnlockNotificationService::class.java)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt index 341f78f22..1e5d7039f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/AttachmentFileNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/AttachmentFileNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.ContentResolver diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt similarity index 94% rename from app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt rename to app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt index 2f4b6e2db..2521db4af 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationField.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationField.kt @@ -17,12 +17,13 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.os.Parcel import android.os.Parcelable import android.util.Log import com.kunzisoft.keepass.model.EntryInfo +import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import java.util.* /** @@ -54,6 +55,7 @@ class ClipboardEntryNotificationField : Parcelable { NotificationFieldId.UNKNOWN -> "" NotificationFieldId.USERNAME -> entryInfo?.username ?: "" NotificationFieldId.PASSWORD -> entryInfo?.password ?: "" + NotificationFieldId.OTP -> entryInfo?.getGeneratedFieldValue(OTP_TOKEN_FIELD) ?: "" NotificationFieldId.FIELD_A, NotificationFieldId.FIELD_B, NotificationFieldId.FIELD_C -> entryInfo?.getGeneratedFieldValue(label) ?: "" @@ -81,7 +83,7 @@ class ClipboardEntryNotificationField : Parcelable { } enum class NotificationFieldId { - UNKNOWN, USERNAME, PASSWORD, FIELD_A, FIELD_B, FIELD_C; + UNKNOWN, USERNAME, PASSWORD, OTP, FIELD_A, FIELD_B, FIELD_C; companion object { val anonymousFieldId: Array diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt similarity index 93% rename from app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt index 9ffd23992..2930f5a78 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/ClipboardEntryNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Context @@ -25,6 +25,7 @@ import android.content.Intent import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.model.EntryInfo +import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER @@ -250,6 +251,7 @@ class ClipboardEntryNotificationService : LockNotificationService() { val containsUsernameToCopy = entry.username.isNotEmpty() val containsPasswordToCopy = entry.password.isNotEmpty() && PreferencesUtil.allowCopyPasswordAndProtectedFields(context) + val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD) val containsExtraFieldToCopy = entry.customFields.isNotEmpty() && (entry.containsCustomFieldsNotProtected() || @@ -262,7 +264,10 @@ class ClipboardEntryNotificationService : LockNotificationService() { // If notifications enabled in settings // Don't if application timeout if (PreferencesUtil.isClipboardNotificationsEnable(context)) { - if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) { + if (containsUsernameToCopy + || containsPasswordToCopy + || containsOTPToCopy + || containsExtraFieldToCopy) { // username already copied, waiting for user's action before copy password. intent.action = ACTION_NEW_NOTIFICATION @@ -282,14 +287,22 @@ class ClipboardEntryNotificationService : LockNotificationService() { ClipboardEntryNotificationField.NotificationFieldId.PASSWORD, context.getString(R.string.entry_password))) } + // Add OTP + if (containsOTPToCopy) { + notificationFields.add( + ClipboardEntryNotificationField( + ClipboardEntryNotificationField.NotificationFieldId.OTP, + OTP_TOKEN_FIELD)) + } // Add extra fields if (containsExtraFieldToCopy) { try { var anonymousFieldNumber = 0 entry.customFields.forEach { field -> //If value is not protected or allowed - if (!field.protectedValue.isProtected - || PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) { + if ((!field.protectedValue.isProtected + || PreferencesUtil.allowCopyPasswordAndProtectedFields(context)) + && field.name != OTP_TOKEN_FIELD) { notificationFields.add( ClipboardEntryNotificationField( ClipboardEntryNotificationField.NotificationFieldId.anonymousFieldId[anonymousFieldNumber], diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt similarity index 84% rename from app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt index bbc0e9bf1..1ee713ef4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/DatabaseTaskNotificationService.kt @@ -17,14 +17,13 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Intent import android.net.Uri -import android.os.Binder -import android.os.Bundle -import android.os.IBinder +import android.os.* +import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper @@ -40,6 +39,8 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -47,6 +48,7 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.closeDatabase +import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import kotlinx.coroutines.* import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -65,6 +67,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private var mAllowFinishAction = AtomicBoolean() private var mActionRunning = false + private var mDatabaseInfoListeners = LinkedList() + private var mIconId: Int = R.drawable.notification_ic_database_load private var mTitleId: Int = R.string.database_opened private var mMessageId: Int? = null @@ -93,6 +97,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress mAllowFinishAction.set(false) } } + + fun addDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) { + mDatabaseInfoListeners.add(databaseInfoListener) + } + + fun removeDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) { + mDatabaseInfoListeners.remove(databaseInfoListener) + } } interface ActionTaskListener { @@ -101,6 +113,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun onStopAction(actionTask: String, result: ActionRunnable.Result) } + interface DatabaseInfoListener { + fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo, + newDatabaseInfo: SnapFileDatabaseInfo) + } + /** * Force to call [ActionTaskListener.onStartAction] if the action is still running */ @@ -112,6 +129,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } + fun checkDatabaseInfo() { + try { + mDatabase.fileUri?.let { + val previousDatabaseInfo = mSnapFileDatabaseInfo + val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( + FileDatabaseInfo(applicationContext, it)) + + val oldDatabaseModification = previousDatabaseInfo?.lastModification + val newDatabaseModification = lastFileDatabaseInfo.lastModification + + val conditionExists = previousDatabaseInfo != null + && previousDatabaseInfo.exists != lastFileDatabaseInfo.exists + // To prevent dialog opening too often + val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null + && oldDatabaseModification < newDatabaseModification + && mLastLocalSaveTime + 5000 < newDatabaseModification) + + if (conditionExists || conditionLastModification) { + // Show the dialog only if it's real new info and not a delay after a save + Log.i(TAG, "Database file modified " + + "$previousDatabaseInfo != $lastFileDatabaseInfo ") + // Call listener to indicate a change in database info + if (previousDatabaseInfo != null) { + mDatabaseInfoListeners.forEach { listener -> + listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo) + } + } + mSnapFileDatabaseInfo = lastFileDatabaseInfo + } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to check database info", e) + } + } + + fun saveDatabaseInfo() { + try { + mDatabase.fileUri?.let { + mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( + FileDatabaseInfo(applicationContext, it)) + Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo") + } + } catch (e: Exception) { + Log.e(TAG, "Unable to check database info", e) + } + } + override fun onBind(intent: Intent): IBinder? { super.onBind(intent) return mActionTaskBinder @@ -138,6 +202,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress val actionRunnable: ActionRunnable? = when (intentAction) { ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent) + ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask() ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent) ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent) @@ -192,6 +257,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress actionTaskListener.onStopAction(intentAction!!, result) } } finally { + // Save the database info before performing action + if (intentAction == ACTION_DATABASE_LOAD_TASK) { + saveDatabaseInfo() + } + // Save the database info after performing save action + if (intentAction == ACTION_DATABASE_SAVE + || intent?.getBooleanExtra(SAVE_DATABASE_KEY, false) == true) { + mDatabase.fileUri?.let { + val newSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( + FileDatabaseInfo(applicationContext, it)) + mLastLocalSaveTime = System.currentTimeMillis() + mSnapFileDatabaseInfo = newSnapFileDatabaseInfo + } + } removeIntentData(intent) TimeoutHelper.releaseTemporarilyDisableTimeout() if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) { @@ -214,7 +293,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } return when (intentAction) { - ACTION_DATABASE_LOAD_TASK, null -> { + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_RELOAD_TASK, + null -> { START_STICKY } else -> { @@ -248,7 +329,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress else -> { when (intentAction) { ACTION_DATABASE_CREATE_TASK -> R.string.creating_database - ACTION_DATABASE_LOAD_TASK -> R.string.loading_database + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database ACTION_DATABASE_SAVE -> R.string.saving_database else -> { R.string.command_execution @@ -258,13 +340,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } mMessageId = when (intentAction) { - ACTION_DATABASE_LOAD_TASK -> null + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_RELOAD_TASK -> null else -> null } mWarningId = if (!saveAction - || intentAction == ACTION_DATABASE_LOAD_TASK) + || intentAction == ACTION_DATABASE_LOAD_TASK + || intentAction == ACTION_DATABASE_RELOAD_TASK) null else R.string.do_not_kill_app @@ -316,10 +400,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress intent?.removeExtra(DATABASE_TASK_WARNING_KEY) intent?.removeExtra(DATABASE_URI_KEY) - intent?.removeExtra(MASTER_PASSWORD_CHECKED_KEY) - intent?.removeExtra(MASTER_PASSWORD_KEY) - intent?.removeExtra(KEY_FILE_CHECKED_KEY) - intent?.removeExtra(KEY_FILE_URI_KEY) + intent?.removeExtra(MAIN_CREDENTIAL_KEY) intent?.removeExtra(READ_ONLY_KEY) intent?.removeExtra(CIPHER_ENTITY_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) @@ -342,9 +423,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress * Execute action with a coroutine */ private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater, - onPreExecute: () -> Unit, - onExecute: (ProgressTaskUpdater?) -> ActionRunnable?, - onPostExecute: (result: ActionRunnable.Result) -> Unit) { + onPreExecute: () -> Unit, + onExecute: (ProgressTaskUpdater?) -> ActionRunnable?, + onPostExecute: (result: ActionRunnable.Result) -> Unit) { mAllowFinishAction.set(false) TimeoutHelper.temporarilyDisableTimeout() @@ -391,13 +472,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private fun buildDatabaseCreateActionTask(intent: Intent): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) - && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) - && intent.hasExtra(MASTER_PASSWORD_KEY) - && intent.hasExtra(KEY_FILE_CHECKED_KEY) - && intent.hasExtra(KEY_FILE_URI_KEY) + && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) - val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY) + val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() if (databaseUri == null) return null @@ -407,14 +485,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress databaseUri, getString(R.string.database_default_name), getString(R.string.database), - intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), - intent.getStringExtra(MASTER_PASSWORD_KEY), - intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), - keyFileUri + mainCredential ) { result -> result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) - putParcelable(KEY_FILE_URI_KEY, keyFileUri) + putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) } } } else { @@ -425,15 +500,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private fun buildDatabaseLoadActionTask(intent: Intent): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) - && intent.hasExtra(MASTER_PASSWORD_KEY) - && intent.hasExtra(KEY_FILE_URI_KEY) + && intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(READ_ONLY_KEY) && intent.hasExtra(CIPHER_ENTITY_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtra(DATABASE_URI_KEY) - val masterPassword: String? = intent.getStringExtra(MASTER_PASSWORD_KEY) - val keyFileUri: Uri? = intent.getParcelableExtra(KEY_FILE_URI_KEY) + val mainCredential: MainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val cipherEntity: CipherDatabaseEntity? = intent.getParcelableExtra(CIPHER_ENTITY_KEY) @@ -444,8 +517,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress this, mDatabase, databaseUri, - masterPassword, - keyFileUri, + mainCredential, readOnly, cipherEntity, intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), @@ -454,8 +526,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress // Add each info to reload database after thrown duplicate UUID exception result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) - putString(MASTER_PASSWORD_KEY, masterPassword) - putParcelable(KEY_FILE_URI_KEY, keyFileUri) + putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(READ_ONLY_KEY, readOnly) putParcelable(CIPHER_ENTITY_KEY, cipherEntity) } @@ -465,21 +536,26 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } + private fun buildDatabaseReloadActionTask(): ActionRunnable { + return ReloadDatabaseRunnable( + this, + mDatabase, + this + ) { result -> + // No need to add each info to reload database + result.data = Bundle() + } + } + private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? { return if (intent.hasExtra(DATABASE_URI_KEY) - && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) - && intent.hasExtra(MASTER_PASSWORD_KEY) - && intent.hasExtra(KEY_FILE_CHECKED_KEY) - && intent.hasExtra(KEY_FILE_URI_KEY) + && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri = intent.getParcelableExtra(DATABASE_URI_KEY) ?: return null AssignPasswordInDatabaseRunnable(this, mDatabase, databaseUri, - intent.getBooleanExtra(MASTER_PASSWORD_CHECKED_KEY, false), - intent.getStringExtra(MASTER_PASSWORD_KEY), - intent.getBooleanExtra(KEY_FILE_CHECKED_KEY, false), - intent.getParcelableExtra(KEY_FILE_URI_KEY) + intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() ) } else { null @@ -770,6 +846,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK" const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK" + const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK" const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK" const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK" const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK" @@ -801,10 +878,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY" const val DATABASE_URI_KEY = "DATABASE_URI_KEY" - const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY" - const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY" - const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY" - const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY" + const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY" const val CIPHER_ENTITY_KEY = "CIPHER_ENTITY_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" @@ -822,6 +896,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time + private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null + private var mLastLocalSaveTime: Long = 0 + fun getListNodesFromBundle(database: Database, bundle: Bundle): List { val nodesAction = ArrayList() bundle.getParcelableArrayList>(GROUPS_ID_KEY)?.forEach { diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt similarity index 99% rename from app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt index 1659bb23d..af214e50b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/KeyboardEntryNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.PendingIntent import android.content.Context diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt similarity index 97% rename from app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt index 2bb4889ec..daf37a124 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/LockNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/LockNotificationService.kt @@ -17,7 +17,7 @@ * along with KeePassDX. If not, see . * */ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.content.Intent import com.kunzisoft.keepass.utils.LockReceiver diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt similarity index 98% rename from app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt rename to app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt index 48b7106c0..e475ad4eb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/NotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt @@ -1,4 +1,4 @@ -package com.kunzisoft.keepass.notifications +package com.kunzisoft.keepass.services import android.app.NotificationChannel import android.app.NotificationManager diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt index cba9c231d..f3462752b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt @@ -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?) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt index d03cd3411..b6b0acb7f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt @@ -103,6 +103,6 @@ class MainPreferenceFragment : PreferenceFragmentCompat() { } interface Callback { - fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) + fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean = false) } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt index d6d14c4f6..b9ebbab67 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt @@ -41,9 +41,10 @@ import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment import com.kunzisoft.keepass.activities.stylish.Stylish import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction -import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper +import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.icons.IconPackChooser +import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService import com.kunzisoft.keepass.settings.preference.IconPackListPreference import com.kunzisoft.keepass.utils.UriUtil @@ -214,9 +215,10 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { val biometricUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.biometric_unlock_enable_key)) val deviceCredentialUnlockEnablePreference: SwitchPreference? = findPreference(getString(R.string.device_credential_unlock_enable_key)) val autoOpenPromptPreference: SwitchPreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key)) + val tempAdvancedUnlockPreference: SwitchPreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key)) val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - BiometricUnlockDatabaseHelper.biometricUnlockSupported(activity) + AdvancedUnlockManager.biometricUnlockSupported(activity) } else false biometricUnlockEnablePreference?.apply { // False if under Marshmallow @@ -237,6 +239,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { deleteKeysMessage(activity) { biometricUnlockEnablePreference.isChecked = false autoOpenPromptPreference?.isEnabled = deviceCredentialChecked + tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked } } else { if (deviceCredentialChecked) { @@ -247,6 +250,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } } else { autoOpenPromptPreference?.isEnabled = true + tempAdvancedUnlockPreference?.isEnabled = true } } true @@ -254,15 +258,18 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } } - val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - BiometricUnlockDatabaseHelper.deviceCredentialUnlockSupported(activity) + val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + AdvancedUnlockManager.deviceCredentialUnlockSupported(activity) } else false deviceCredentialUnlockEnablePreference?.apply { + // Biometric unlock already checked + if (biometricUnlockEnablePreference?.isChecked == true) + isChecked = false if (!deviceCredentialUnlockSupported) { isChecked = false setOnPreferenceClickListener { preference -> (preference as SwitchPreference).isChecked = false - UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.R) + UnavailableFeatureDialogFragment.getInstance(Build.VERSION_CODES.M) .show(parentFragmentManager, "unavailableFeatureDialog") false } @@ -275,6 +282,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { deleteKeysMessage(activity) { deviceCredentialUnlockEnablePreference.isChecked = false autoOpenPromptPreference?.isEnabled = biometricChecked + tempAdvancedUnlockPreference?.isEnabled = biometricChecked } } else { if (biometricChecked) { @@ -285,6 +293,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } } else { autoOpenPromptPreference?.isEnabled = true + tempAdvancedUnlockPreference?.isEnabled = true } } true @@ -294,6 +303,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true || deviceCredentialUnlockEnablePreference?.isChecked == true + tempAdvancedUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true + || deviceCredentialUnlockEnablePreference?.isChecked == true + + tempAdvancedUnlockPreference?.setOnPreferenceClickListener { + tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked + deleteKeysMessage(activity) { + tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked + } + true + } val deleteKeysFingerprints: Preference? = findPreference(getString(R.string.biometric_delete_all_key_key)) if (biometricUnlockSupported || deviceCredentialUnlockSupported) { @@ -321,9 +340,9 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { validate?.invoke() deleteKeysAlertDialog?.setOnDismissListener(null) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - BiometricUnlockDatabaseHelper.deleteEntryKeyInKeystoreForBiometric( + AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric( activity, - object : BiometricUnlockDatabaseHelper.BiometricUnlockErrorCallback { + object : AdvancedUnlockManager.AdvancedUnlockErrorCallback { fun showException(e: Exception) { Toast.makeText(context, getString(R.string.advanced_unlock_scanning_error, e.localizedMessage), @@ -334,11 +353,12 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { showException(e) } - override fun onBiometricException(e: Exception) { + override fun onGenericException(e: Exception) { showException(e) } }) } + AdvancedUnlockNotificationService.stopService(activity.applicationContext) CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll() } .setNegativeButton(resources.getString(android.R.string.cancel) @@ -366,7 +386,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } if (styleEnabled) { Stylish.assignStyle(styleIdString) - activity.recreate() + // Relaunch the current activity to redraw theme + (activity as? SettingsActivity?)?.apply { + keepCurrentScreen() + startActivity(intent) + finish() + activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } } styleEnabled } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt index de0bbadda..fe9a26dc9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt @@ -35,7 +35,7 @@ import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm -import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.settings.preference.* import com.kunzisoft.keepass.settings.preferencedialogfragment.* import com.kunzisoft.keepass.tasks.ActionRunnable @@ -58,7 +58,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() { private var mEncryptionAlgorithmPref: DialogListExplanationPreference? = null private var mKeyDerivationPref: DialogListExplanationPreference? = null private var mRoundPref: InputKdfNumberPreference? = null - private var mMemoryPref: InputKdfNumberPreference? = null + private var mMemoryPref: InputKdfSizePreference? = null private var mParallelismPref: InputKdfNumberPreference? = null override fun onCreateScreenPreference(screen: Screen, savedInstanceState: Bundle?, rootKey: String?) { @@ -231,7 +231,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() { } // Memory Usage - mMemoryPref = findPreference(getString(R.string.memory_usage_key))?.apply { + mMemoryPref = findPreference(getString(R.string.memory_usage_key))?.apply { summary = mDatabase.memoryUsage.toString() } @@ -552,6 +552,13 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() { settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly) true } + R.id.menu_reload_database -> { + settingActivity?.apply { + keepCurrentScreen() + mProgressDatabaseTaskProvider?.startDatabaseReload(false) + } + return true + } else -> { // Check the time lock before launching settings diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt index 2cc7215f7..857921440 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.kt @@ -35,9 +35,13 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() { APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY } + fun getScreen(): Screen { + return Screen.values()[requireArguments().getInt(TAG_KEY)] + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { onCreateScreenPreference( - Screen.values()[requireArguments().getInt(TAG_KEY)], + getScreen(), savedInstanceState, rootKey) } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index f6d9bf2be..f2c166d9e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -19,12 +19,14 @@ */ package com.kunzisoft.keepass.settings +import android.app.backup.BackupManager import android.content.Context import android.content.res.Resources import android.net.Uri import androidx.preference.PreferenceManager import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.timeout.TimeoutHelper import java.util.* @@ -43,6 +45,7 @@ object PreferencesUtil { } apply() } + BackupManager(context).dataChanged() } fun getDefaultDatabasePath(context: Context): String? { @@ -201,6 +204,13 @@ object PreferencesUtil { ?: TimeoutHelper.DEFAULT_TIMEOUT } + fun getAdvancedUnlockTimeout(context: Context): Long { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getString(context.getString(R.string.temp_advanced_unlock_timeout_key), + context.getString(R.string.temp_advanced_unlock_timeout_default))?.toLong() + ?: TimeoutHelper.DEFAULT_TIMEOUT + } + fun isLockDatabaseWhenScreenShutOffEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.lock_database_screen_off_key), @@ -231,8 +241,29 @@ object PreferencesUtil { fun isBiometricUnlockEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + AdvancedUnlockManager.biometricUnlockSupported(context) + } else { + false + } return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), context.resources.getBoolean(R.bool.biometric_unlock_enable_default)) + && biometricSupported + } + + fun isDeviceCredentialUnlockEnable(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + // Priority to biometric unlock + val biometricAlreadySupported = isBiometricUnlockEnable(context) + return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key), + context.resources.getBoolean(R.bool.device_credential_unlock_enable_default)) + && !biometricAlreadySupported + } + + fun isTempAdvancedUnlockEnable(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.temp_advanced_unlock_enable_key), + context.resources.getBoolean(R.bool.temp_advanced_unlock_enable_default)) } fun isAdvancedUnlockPromptAutoOpenEnable(context: Context): Boolean { @@ -241,12 +272,6 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default)) } - fun isDeviceCredentialUnlockEnable(context: Context): Boolean { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key), - context.resources.getBoolean(R.bool.device_credential_unlock_enable_default)) - } - fun getListSort(context: Context): SortNodeEnum { val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.getString(context.getString(R.string.sort_node_key), @@ -411,13 +436,18 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.autofill_close_database_default)) } - fun isAutofillAutoSearchEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key), context.resources.getBoolean(R.bool.autofill_auto_search_default)) } + fun isAutofillInlineSuggestionsEnable(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key), + context.resources.getBoolean(R.bool.autofill_inline_suggestions_default)) + } + fun isAutofillSaveSearchInfoEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key), diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt index 2188498a2..4760e86c2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. + * Copyright 2020 Jeremy Jamet / Kunzisoft. * * This file is part of KeePassDX. * @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings import android.app.Activity import android.app.backup.BackupManager -import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle @@ -35,9 +34,12 @@ import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.view.showActionError +import com.kunzisoft.keepass.view.showActionErrorIfNeeded open class SettingsActivity : LockingActivity(), @@ -81,7 +83,7 @@ open class SettingsActivity } // Focus view to reinitialize timeout - resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout) + coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() @@ -94,12 +96,35 @@ open class SettingsActivity backupManager = BackupManager(this) mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result -> - // Call result in fragment - (supportFragmentManager - .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) - ?.onProgressDialogThreadResult(actionTask, result) + when (actionTask) { + DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { + // Reload the current activity + if (result.isSuccess) { + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } else { + this.showActionErrorIfNeeded(result) + finish() + } + } + else -> { + // Call result in fragment + (supportFragmentManager + .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) + ?.onProgressDialogThreadResult(actionTask, result) + } + } + coordinatorLayout?.showActionErrorIfNeeded(result) + } - coordinatorLayout?.showActionError(result) + // To reload the current screen + if (intent.extras?.containsKey(FRAGMENT_ARG) == true) { + intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName -> + onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true) + } + // Eat state + intent.removeExtra(FRAGMENT_ARG) } } @@ -117,52 +142,33 @@ open class SettingsActivity } override fun onPasswordEncodingValidateListener(databaseUri: Uri?, - masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + mainCredential: MainCredential) { databaseUri?.let { mProgressDatabaseTaskProvider?.startDatabaseAssignPassword( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } } - override fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) { + override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { Database.getInstance().let { database -> database.fileUri?.let { databaseUri -> // Show the progress dialog now or after dialog confirmation - if (database.validatePasswordEncoding(masterPassword, keyFileChecked)) { + if (database.validatePasswordEncoding(mainCredential)) { mProgressDatabaseTaskProvider?.startDatabaseAssignPassword( databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile + mainCredential ) } else { - PasswordEncodingDialogFragment.getInstance(databaseUri, - masterPasswordChecked, - masterPassword, - keyFileChecked, - keyFile - ).show(supportFragmentManager, "passwordEncodingTag") + PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential) + .show(supportFragmentManager, "passwordEncodingTag") } } } } - override fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, - masterPassword: String?, - keyFileChecked: Boolean, - keyFile: Uri?) {} + override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {} private fun hideOrShowLockButton(key: NestedSettingsFragment.Screen) { if (PreferencesUtil.showLockDatabaseButton(this)) { @@ -192,25 +198,41 @@ open class SettingsActivity hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION) } - private fun replaceFragment(key: NestedSettingsFragment.Screen) { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, + private fun replaceFragment(key: NestedSettingsFragment.Screen, reload: Boolean) { + supportFragmentManager.beginTransaction().apply { + if (reload) { + setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, R.anim.slide_in_left, R.anim.slide_out_right) - .replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED) - .addToBackStack(TAG_NESTED) - .commit() + } else { + setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, + R.anim.slide_in_left, R.anim.slide_out_right) + } + replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED) + addToBackStack(TAG_NESTED) + commit() + } toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key) hideOrShowLockButton(key) } - override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) { + /** + * To keep the current screen when activity is reloaded + */ + fun keepCurrentScreen() { + (supportFragmentManager.findFragmentByTag(TAG_NESTED) as? NestedSettingsFragment?) + ?.getScreen()?.let { fragmentKey -> + intent.putExtra(FRAGMENT_ARG, fragmentKey.name) + } + } + + override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) { if (mTimeoutEnable) TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { - replaceFragment(key) + replaceFragment(key, reload) } else - replaceFragment(key) + replaceFragment(key, reload) } override fun onSaveInstanceState(outState: Bundle) { @@ -225,6 +247,7 @@ open class SettingsActivity private const val SHOW_LOCK = "SHOW_LOCK" private const val TITLE_KEY = "TITLE_KEY" private const val TAG_NESTED = "TAG_NESTED" + private const val FRAGMENT_ARG = "FRAGMENT_ARG" fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) { val intent = Intent(activity, SettingsActivity::class.java) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt index 9a8376f02..766f3d634 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt @@ -25,7 +25,7 @@ import androidx.preference.DialogPreference import com.kunzisoft.keepass.R import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine -class InputKdfNumberPreference @JvmOverloads constructor(context: Context, +open class InputKdfNumberPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleRes: Int = defStyleAttr) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt new file mode 100644 index 000000000..9fe61e3a3 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preference + +import android.content.Context +import android.util.AttributeSet +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.DataByte + +class InputKdfSizePreference @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleRes: Int = defStyleAttr) + : InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) { + + override fun setSummary(summary: CharSequence) { + if (summary == UNKNOWN_VALUE_STRING) { + super.setSummary("") + } else { + var summaryString = summary + try { + val memorySize = summary.toString().toLong() + summaryString = if (memorySize > 0) { + // To convert bytes to mebibytes + DataByte(memorySize, DataByte.ByteFormat.BYTE) + .toBetterByteFormat().toString(context) + } else { + memorySize.toString() + } + } catch (e: Exception) { + } finally { + super.setSummary(summaryString) + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt index be27d4d65..e0fe8da97 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt @@ -36,7 +36,7 @@ open class InputNumberPreference @JvmOverloads constructor(context: Context, override fun setSummary(summary: CharSequence) { if (summary == INFINITE_VALUE_STRING) { - super.setSummary("") + super.setSummary("∞") } else { super.setSummary(summary) } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt new file mode 100644 index 000000000..90b1e42d1 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.settings.preference + +import android.content.Context +import android.util.AttributeSet +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.DataByte + +open class InputSizePreference @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleRes: Int = defStyleAttr) + : InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) { + + override fun setSummary(summary: CharSequence) { + var summaryString = summary + try { + val memorySize = summary.toString().toLong() + summaryString = if (memorySize >= 0) { + // To convert bytes to mebibytes + DataByte(memorySize, DataByte.ByteFormat.BYTE) + .toBetterByteFormat().toString(context) + } else { + memorySize.toString() + } + } catch (e: Exception) { + } finally { + super.setSummary(summaryString) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt index 7932e3d3d..11751d2fb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/InputPreferenceDialogFragmentCompat.kt @@ -33,6 +33,7 @@ import com.kunzisoft.keepass.R abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() { private var inputTextView: EditText? = null + private var textUnitView: TextView? = null private var textExplanationView: TextView? = null private var switchElementView: CompoundButton? = null @@ -47,6 +48,14 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom } } + fun setInoutText(@StringRes inputTextId: Int) { + inputText = getString(inputTextId) + } + + fun showInputText(show: Boolean) { + inputTextView?.visibility = if (show) View.VISIBLE else View.GONE + } + fun setInputTextError(error: CharSequence) { this.inputTextView?.error = error } @@ -55,6 +64,24 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom this.mOnInputTextEditorActionListener = onEditorActionListener } + var unitText: String? + get() = textUnitView?.text?.toString() ?: "" + set(unitText) { + textUnitView?.apply { + if (unitText != null && unitText.isNotEmpty()) { + text = unitText + visibility = View.VISIBLE + } else { + text = "" + visibility = View.GONE + } + } + } + + fun setUnitText(@StringRes unitTextId: Int) { + unitText = getString(unitTextId) + } + var explanationText: String? get() = textExplanationView?.text?.toString() ?: "" set(explanationText) { @@ -69,6 +96,10 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom } } + fun setExplanationText(@StringRes explanationTextId: Int) { + explanationText = getString(explanationTextId) + } + override fun onBindDialogView(view: View) { super.onBindDialogView(view) @@ -93,6 +124,8 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom } } } + textUnitView = view.findViewById(R.id.input_text_unit) + textUnitView?.visibility = View.GONE textExplanationView = view.findViewById(R.id.explanation_text) textExplanationView?.visibility = View.GONE switchElementView = view.findViewById(R.id.switch_element) @@ -113,18 +146,6 @@ abstract class InputPreferenceDialogFragmentCompat : PreferenceDialogFragmentCom return false } - fun setInoutText(@StringRes inputTextId: Int) { - inputText = getString(inputTextId) - } - - fun showInputText(show: Boolean) { - inputTextView?.visibility = if (show) View.VISIBLE else View.GONE - } - - fun setExplanationText(@StringRes explanationTextId: Int) { - explanationText = getString(explanationTextId) - } - fun setSwitchAction(onCheckedChange: ((isChecked: Boolean)-> Unit)?, defaultChecked: Boolean) { switchElementView?.visibility = if (onCheckedChange == null) View.GONE else View.VISIBLE switchElementView?.isChecked = defaultChecked diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt index 61a0cb9d1..4a15d806e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MaxHistorySizePreferenceDialogFragmentCompat.kt @@ -22,50 +22,76 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.os.Bundle import android.view.View import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.DataByte class MaxHistorySizePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + private var dataByte = DataByte(2L, DataByte.ByteFormat.MEBIBYTE) + override fun onBindDialogView(view: View) { super.onBindDialogView(view) setExplanationText(R.string.max_history_size_summary) database?.historyMaxSize?.let { maxItemsDatabase -> - inputText = maxItemsDatabase.toString() + dataByte = DataByte(maxItemsDatabase, DataByte.ByteFormat.BYTE) + .toBetterByteFormat() + inputText = dataByte.number.toString() + if (dataByte.number >= 0) { + setUnitText(dataByte.format.stringId) + } else { + unitText = null + } + setSwitchAction({ isChecked -> - inputText = if (!isChecked) { - INFINITE_MAX_HISTORY_SIZE.toString() - } else - DEFAULT_MAX_HISTORY_SIZE.toString() + if (!isChecked) { + dataByte = INFINITE_MAX_HISTORY_SIZE_DATA_BYTE + inputText = INFINITE_MAX_HISTORY_SIZE.toString() + unitText = null + } else { + dataByte = DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE + inputText = dataByte.number.toString() + setUnitText(dataByte.format.stringId) + } showInputText(isChecked) }, maxItemsDatabase > INFINITE_MAX_HISTORY_SIZE) + } } override fun onDialogClosed(positiveResult: Boolean) { if (positiveResult) { database?.let { database -> - var maxHistorySize: Long = try { + val maxHistorySize: Long = try { inputText.toLong() } catch (e: NumberFormatException) { - DEFAULT_MAX_HISTORY_SIZE + DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE.toBytes() } - if (maxHistorySize < INFINITE_MAX_HISTORY_SIZE) { - maxHistorySize = INFINITE_MAX_HISTORY_SIZE + val numberOfBytes = if (maxHistorySize >= 0) { + val dataByteConversion = DataByte(maxHistorySize, dataByte.format) + var bytes = dataByteConversion.toBytes() + if (bytes > Long.MAX_VALUE) { + bytes = Long.MAX_VALUE + } + bytes + } else { + INFINITE_MAX_HISTORY_SIZE } val oldMaxHistorySize = database.historyMaxSize - database.historyMaxSize = maxHistorySize + database.historyMaxSize = numberOfBytes - mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, maxHistorySize, mDatabaseAutoSaveEnable) + mProgressDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(oldMaxHistorySize, numberOfBytes, mDatabaseAutoSaveEnable) } } } companion object { - const val DEFAULT_MAX_HISTORY_SIZE = 134217728L const val INFINITE_MAX_HISTORY_SIZE = -1L + private val INFINITE_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(INFINITE_MAX_HISTORY_SIZE, DataByte.ByteFormat.MEBIBYTE) + private val DEFAULT_MAX_HISTORY_SIZE_DATA_BYTE = DataByte(6L, DataByte.ByteFormat.MEBIBYTE) + fun newInstance(key: String): MaxHistorySizePreferenceDialogFragmentCompat { val fragment = MaxHistorySizePreferenceDialogFragmentCompat() val bundle = Bundle(1) diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt index 11cd7dc8e..184989436 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preferencedialogfragment/MemoryUsagePreferenceDialogFragmentCompat.kt @@ -22,33 +22,46 @@ package com.kunzisoft.keepass.settings.preferencedialogfragment import android.os.Bundle import android.view.View import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.utils.DataByte class MemoryUsagePreferenceDialogFragmentCompat : DatabaseSavePreferenceDialogFragmentCompat() { + private var dataByte = DataByte(MIN_MEMORY_USAGE, DataByte.ByteFormat.BYTE) + override fun onBindDialogView(view: View) { super.onBindDialogView(view) setExplanationText(R.string.memory_usage_explanation) - inputText = database?.memoryUsage?.toString()?: MIN_MEMORY_USAGE.toString() + + val memoryBytes = database?.memoryUsage ?: MIN_MEMORY_USAGE + dataByte = DataByte(memoryBytes, DataByte.ByteFormat.BYTE) + .toBetterByteFormat() + inputText = dataByte.number.toString() + setUnitText(dataByte.format.stringId) } override fun onDialogClosed(positiveResult: Boolean) { if (positiveResult) { database?.let { database -> - var memoryUsage: Long = try { + var newMemoryUsage: Long = try { inputText.toLong() } catch (e: NumberFormatException) { MIN_MEMORY_USAGE } - if (memoryUsage < MIN_MEMORY_USAGE) { - memoryUsage = MIN_MEMORY_USAGE + if (newMemoryUsage < MIN_MEMORY_USAGE) { + newMemoryUsage = MIN_MEMORY_USAGE + } + // To transform in bytes + dataByte.number = newMemoryUsage + var numberOfBytes = dataByte.toBytes() + if (numberOfBytes > Long.MAX_VALUE) { + numberOfBytes = Long.MAX_VALUE } - // TODO Max Memory val oldMemoryUsage = database.memoryUsage - database.memoryUsage = memoryUsage + database.memoryUsage = numberOfBytes - mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, memoryUsage, mDatabaseAutoSaveEnable) + mProgressDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(oldMemoryUsage, numberOfBytes, mDatabaseAutoSaveEnable) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt b/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt index b071c33d6..084b12bd2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/tasks/AttachmentFileBinderManager.kt @@ -31,10 +31,10 @@ import androidx.fragment.app.FragmentActivity import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD -import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE +import com.kunzisoft.keepass.services.AttachmentFileNotificationService +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD +import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_REMOVE class AttachmentFileBinderManager(private val activity: FragmentActivity) { diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt index 59c97ac7c..c12fb0838 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt @@ -32,8 +32,8 @@ import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.magikeyboard.MagikIME -import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService -import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService +import com.kunzisoft.keepass.services.ClipboardEntryNotificationService +import com.kunzisoft.keepass.services.KeyboardEntryNotificationService import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -138,5 +138,5 @@ fun Context.closeDatabase() { cancelAll() } // Clear data - Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) + Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt b/app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt new file mode 100644 index 000000000..4a8e53d6c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/utils/DataByte.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.utils + +import android.content.Context +import androidx.annotation.StringRes +import com.kunzisoft.keepass.R + +class DataByte(var number: Long, var format: ByteFormat) { + + fun toBetterByteFormat(): DataByte { + return when (this.format) { + ByteFormat.BYTE -> { + when { + //this.number % GIBIBYTES == 0L -> { + // DataByte((this.number / GIBIBYTES), ByteFormat.GIBIBYTE) + //} + this.number % MEBIBYTES == 0L -> { + DataByte((this.number / MEBIBYTES), ByteFormat.MEBIBYTE) + } + this.number % KIBIBYTES == 0L -> { + DataByte((this.number / KIBIBYTES), ByteFormat.KIBIBYTE) + } + else -> { + DataByte(this.number, ByteFormat.BYTE) + } + } + } + else -> { + DataByte(toBytes(), ByteFormat.BYTE).toBetterByteFormat() + } + } + } + + /** + * Number of bytes in current DataByte + */ + fun toBytes(): Long { + return when (this.format) { + ByteFormat.BYTE -> this.number + ByteFormat.KIBIBYTE -> this.number * KIBIBYTES + ByteFormat.MEBIBYTE -> this.number * MEBIBYTES + //ByteFormat.GIBIBYTE -> this.number * GIBIBYTES + } + } + + override fun toString(): String { + return "$number ${format.name}" + } + + fun toString(context: Context): String { + return "$number ${context.getString(format.stringId)}" + } + + enum class ByteFormat(@StringRes var stringId: Int) { + BYTE(R.string.unit_byte), + KIBIBYTE(R.string.unit_kibibyte), + MEBIBYTE(R.string.unit_mebibyte) + //GIBIBYTE(R.string.unit_gibibyte) + } + + companion object { + const val KIBIBYTES = 1024L + const val MEBIBYTES = 1048576L + const val GIBIBYTES = 1073741824L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt index 63d5cb7dc..38a311fc2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.kt @@ -53,23 +53,19 @@ object MenuUtil { fun onDefaultMenuOptionsItemSelected(activity: Activity, item: MenuItem, readOnly: Boolean = READ_ONLY_DEFAULT, - timeoutEnable: Boolean = false): Boolean { + timeoutEnable: Boolean = false) { when (item.itemId) { R.id.menu_contribute -> { onContributionItemSelected(activity) - return true } R.id.menu_app_settings -> { // To avoid flickering when launch settings in a LockingActivity SettingsActivity.launch(activity, readOnly, timeoutEnable) - return true } R.id.menu_about -> { val intent = Intent(activity, AboutActivity::class.java) activity.startActivity(intent) - return true } - else -> return true } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt new file mode 100644 index 000000000..90aeb12ac --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt @@ -0,0 +1,14 @@ +package com.kunzisoft.keepass.utils + +object StringUtil { + + fun String.removeLineChars(): String { + return this.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "") + } + + fun String.removeSpaceChars(): String { + return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "") + } + + fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt index 974eab131..e54399b2a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt @@ -48,8 +48,8 @@ import java.util.* class EntryContentsView @JvmOverloads constructor(context: Context, - var attrs: AttributeSet? = null, - var defStyle: Int = 0) + attrs: AttributeSet? = null, + defStyle: Int = 0) : LinearLayout(context, attrs, defStyle) { private var fontInVisibility: Boolean = false @@ -67,7 +67,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context, private val creationDateView: TextView private val modificationDateView: TextView - private val lastAccessDateView: TextView private val expiresImageView: ImageView private val expiresDateView: TextView @@ -117,7 +116,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context, creationDateView = findViewById(R.id.entry_created) modificationDateView = findViewById(R.id.entry_modified) - lastAccessDateView = findViewById(R.id.entry_accessed) expiresImageView = findViewById(R.id.entry_expires_image) expiresDateView = findViewById(R.id.entry_expires_date) @@ -258,20 +256,13 @@ class EntryContentsView @JvmOverloads constructor(context: Context, modificationDateView.text = date.getDateTimeString(resources) } - fun assignLastAccessDate(date: DateInstant) { - lastAccessDateView.text = date.getDateTimeString(resources) - } - - fun setExpires(isExpires: Boolean) { + fun setExpires(isExpires: Boolean, expiryTime: DateInstant) { expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE - } - - fun assignExpiresDate(date: DateInstant) { - assignExpiresDate(date.getDateTimeString(resources)) - } - - fun assignExpiresDate(constString: String) { - expiresDateView.text = constString + expiresDateView.text = if (isExpires) { + expiryTime.getDateTimeString(resources) + } else { + resources.getString(R.string.never) + } } fun assignUUID(uuid: UUID) { @@ -279,7 +270,6 @@ class EntryContentsView @JvmOverloads constructor(context: Context, uuidReferenceView.text = UuidUtil.toHexString(uuid) } - fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) { passwordFieldView.hiddenProtectedValue = hiddenProtectedValue // Hidden style for custom fields @@ -306,7 +296,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context, allowCopy: Boolean, onCopyButtonClickListener: OnClickListener?) { - val entryCustomField: EntryField? = EntryField(context, attrs, defStyle) + val entryCustomField: EntryField? = EntryField(context) entryCustomField?.apply { setLabel(title) setValue(value.toString(), value.isProtected) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/KeyFileSelectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/KeyFileSelectionView.kt index cee87171a..1dc1f370d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/KeyFileSelectionView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/KeyFileSelectionView.kt @@ -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?) { diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt index 2e25e9192..cc86a0296 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt @@ -31,7 +31,7 @@ import com.kunzisoft.keepass.R class ToolbarAction @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyle: Int = androidx.appcompat.R.attr.toolbarStyle) + defStyle: Int = R.attr.actionToolbarAppearance) : Toolbar(context, attrs, defStyle) { private var mActionModeCallback: ActionMode.Callback? = null @@ -39,7 +39,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context, private var isOpen = false init { - visibility = View.GONE + setNavigationIcon(R.drawable.ic_close_white_24dp) } fun startSupportActionMode(actionModeCallback: ActionMode.Callback): ActionMode { @@ -55,8 +55,6 @@ class ToolbarAction @JvmOverloads constructor(context: Context, actionMode.finish() } - setNavigationIcon(R.drawable.ic_close_white_24dp) - open() return actionMode diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 720b6cfd7..df734a796 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.view import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator +import android.content.Context import android.graphics.Color import android.graphics.Paint import android.graphics.Typeface @@ -35,6 +36,7 @@ import android.text.style.ClickableSpan import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import android.widget.TextView +import android.widget.Toast import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.updatePadding import com.google.android.material.snackbar.Snackbar @@ -166,7 +168,17 @@ fun View.updateLockPaddingLeft() { )) } -fun CoordinatorLayout.showActionError(result: ActionRunnable.Result) { +fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) { + if (!result.isSuccess) { + result.exception?.errorId?.let { errorId -> + Toast.makeText(this, errorId, Toast.LENGTH_LONG).show() + } ?: result.message?.let { message -> + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + } +} + +fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) { if (!result.isSuccess) { result.exception?.errorId?.let { errorId -> Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show() diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFileViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFileViewModel.kt index 449f5da10..ffc8f8d8d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFileViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFileViewModel.kt @@ -4,8 +4,12 @@ import android.app.Application import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData +import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +import com.kunzisoft.keepass.app.database.IOActionTask import com.kunzisoft.keepass.model.DatabaseFile +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.UriUtil class DatabaseFileViewModel(application: Application) : AndroidViewModel(application) { @@ -15,6 +19,33 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(application.applicationContext) } + val isDefaultDatabase: MutableLiveData by lazy { + MutableLiveData() + } + + fun checkIfIsDefaultDatabase(databaseUri: Uri) { + IOActionTask( + { + (UriUtil.parse(PreferencesUtil.getDefaultDatabasePath(getApplication().applicationContext)) + == databaseUri) + }, + { + isDefaultDatabase.value = it + } + ).execute() + } + + fun removeDefaultDatabase() { + IOActionTask( + { + PreferencesUtil.saveDefaultDatabasePath(getApplication().applicationContext, + null) + }, + { + } + ).execute() + } + val databaseFileLoaded: MutableLiveData by lazy { MutableLiveData() } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt index 28e628589..a69d42895 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/DatabaseFilesViewModel.kt @@ -1,7 +1,6 @@ package com.kunzisoft.keepass.viewmodels import android.app.Application -import android.app.backup.BackupManager import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData @@ -42,11 +41,8 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic fun setDefaultDatabase(databaseFile: DatabaseFile?) { IOActionTask( { - val context = getApplication().applicationContext - UriUtil.parse(PreferencesUtil.getDefaultDatabasePath(context)) - PreferencesUtil.saveDefaultDatabasePath(context, databaseFile?.databaseUri) - val backupManager = BackupManager(context) - backupManager.dataChanged() + PreferencesUtil.saveDefaultDatabasePath(getApplication().applicationContext, + databaseFile?.databaseUri) }, { checkDefaultDatabase() diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt index 65b8c9e5b..6d9df989c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt @@ -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()) diff --git a/app/src/main/jni/argon2/argon2_jni.c b/app/src/main/jni/argon2/argon2_jni.c index 64203f9e4..caada5e43 100644 --- a/app/src/main/jni/argon2/argon2_jni.c +++ b/app/src/main/jni/argon2/argon2_jni.c @@ -129,7 +129,7 @@ void throwExceptionF(JNIEnv *env, jclass exception, const char *format, ...) { JNIEXPORT jbyteArray JNICALL Java_com_kunzisoft_keepass_crypto_keyDerivation_Argon2Native_nTransformMasterKey(JNIEnv *env, - jobject this, jbyteArray password, jbyteArray salt, jint parallelism, jint memory, + jobject this, jint type, jbyteArray password, jbyteArray salt, jint parallelism, jint memory, jint iterations, jbyteArray secretKey, jbyteArray associatedData, jint version) { argon2_context context; @@ -169,7 +169,7 @@ JNICALL Java_com_kunzisoft_keepass_crypto_keyDerivation_Argon2Native_nTransformM context.flags = ARGON2_DEFAULT_FLAGS; context.version = (uint32_t) version; - int argonResult = argon2_ctx(&context, Argon2_d); + int argonResult = argon2_ctx(&context, (argon2_type) type); jbyteArray result; if (argonResult != ARGON2_OK) { diff --git a/app/src/main/jni/argon2/src/argon2.c b/app/src/main/jni/argon2/src/argon2.c index 9d294fd21..bf73d3935 100644 --- a/app/src/main/jni/argon2/src/argon2.c +++ b/app/src/main/jni/argon2/src/argon2.c @@ -25,6 +25,7 @@ const char *argon2_type2string(argon2_type type, int uppercase) { switch (type) { + default: case Argon2_d: return uppercase ? "Argon2d" : "argon2d"; case Argon2_i: diff --git a/app/src/main/res/drawable/ic_reload_white_24dp.xml b/app/src/main/res/drawable/ic_reload_white_24dp.xml new file mode 100644 index 000000000..020472b89 --- /dev/null +++ b/app/src/main/res/drawable/ic_reload_white_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/notification_ic_device_unlock_24dp.xml b/app/src/main/res/drawable/notification_ic_device_unlock_24dp.xml new file mode 100644 index 000000000..238cce5b4 --- /dev/null +++ b/app/src/main/res/drawable/notification_ic_device_unlock_24dp.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/notification_ic_fingerprint_unlock_24dp.xml b/app/src/main/res/drawable/notification_ic_fingerprint_unlock_24dp.xml new file mode 100644 index 000000000..a03cce62b --- /dev/null +++ b/app/src/main/res/drawable/notification_ic_fingerprint_unlock_24dp.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry_edit.xml b/app/src/main/res/layout/activity_entry_edit.xml index e04e5ac54..285fa5bfc 100644 --- a/app/src/main/res/layout/activity_entry_edit.xml +++ b/app/src/main/res/layout/activity_entry_edit.xml @@ -33,27 +33,14 @@ android:layout_height="wrap_content" android:fitsSystemWindows="true"> - - - - - + android:layout_height="?attr/actionBarSize" + android:theme="?attr/specialToolbarAppearance" + app:titleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.Title" + app:subtitleTextAppearance="@style/KeepassDXStyle.TextAppearance.Toolbar.Special.SubTitle" + app:layout_constraintTop_toTopOf="parent" /> @@ -79,24 +66,22 @@ - + app:layout_constraintBottom_toBottomOf="parent" /> - - - + android:layout_height="match_parent" /> diff --git a/app/src/main/res/layout/fragment_advanced_unlock.xml b/app/src/main/res/layout/fragment_advanced_unlock.xml new file mode 100644 index 000000000..f9e736982 --- /dev/null +++ b/app/src/main/res/layout/fragment_advanced_unlock.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_entry_edit_contents.xml b/app/src/main/res/layout/fragment_entry_edit_contents.xml index 3fca6e1bd..2fb95c7f9 100644 --- a/app/src/main/res/layout/fragment_entry_edit_contents.xml +++ b/app/src/main/res/layout/fragment_entry_edit_contents.xml @@ -106,6 +106,7 @@ android:inputType="textPassword|textMultiLine" android:importantForAccessibility="no" android:importantForAutofill="no" + android:imeOptions="flagNoPersonalizedLearning" android:ems="10" android:maxLines="10" android:hint="@string/entry_password"/> diff --git a/app/src/main/res/layout/fragment_set_otp.xml b/app/src/main/res/layout/fragment_set_otp.xml index 6ffc6e9d3..c644902b1 100644 --- a/app/src/main/res/layout/fragment_set_otp.xml +++ b/app/src/main/res/layout/fragment_set_otp.xml @@ -29,6 +29,18 @@ android:layout_height="wrap_content" android:importantForAutofill="noExcludeDescendants" tools:targetApi="o"> + + + diff --git a/app/src/main/res/layout/pref_dialog_input_numbers.xml b/app/src/main/res/layout/pref_dialog_input_numbers.xml index 1e466e59c..8e725ccd3 100644 --- a/app/src/main/res/layout/pref_dialog_input_numbers.xml +++ b/app/src/main/res/layout/pref_dialog_input_numbers.xml @@ -46,14 +46,24 @@ app:layout_constraintTop_toBottomOf="@+id/explanation_text" app:layout_constraintStart_toStartOf="parent" android:minHeight="48dp"/> + + android:inputType="number" + android:minHeight="48dp" + app:layout_constraintEnd_toStartOf="@+id/input_text_unit" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/switch_element" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_entry_contents.xml b/app/src/main/res/layout/view_entry_contents.xml index c5922f214..48164dfa5 100644 --- a/app/src/main/res/layout/view_entry_contents.xml +++ b/app/src/main/res/layout/view_entry_contents.xml @@ -175,12 +175,6 @@ android:layout_height="wrap_content" android:text="@string/entry_accessed" style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" /> - + app:layout_constraintEnd_toEndOf="parent"> - - \ No newline at end of file diff --git a/app/src/main/res/menu/contribution.xml b/app/src/main/res/menu/contribution.xml index b76875ff9..5c1671456 100644 --- a/app/src/main/res/menu/contribution.xml +++ b/app/src/main/res/menu/contribution.xml @@ -22,6 +22,6 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/database.xml b/app/src/main/res/menu/database.xml index d94f86817..a7a182b8a 100644 --- a/app/src/main/res/menu/database.xml +++ b/app/src/main/res/menu/database.xml @@ -24,4 +24,9 @@ android:title="@string/menu_save_database" android:orderInCategory="95" app:showAsAction="ifRoom" /> + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 51b1a3342..d31b3c541 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -103,7 +103,6 @@ هل أنت متأكد من أنك لا تريد استخدام أي مفتاح تشفير ؟ الإصدار %1$s أضف عناصر جديدة إلى قاعدتك - قم بفتح قاعدة بياناتك ببصمتك إضافة حقول مخصصة نسخ حقل تأمين قاعدة البيانات @@ -114,7 +113,7 @@ وظيفة اشتقاق المفتاح مهلة التطبيق مدة الانتظار قبل إقفال قاعدة البيانات - تصفَّح الملفات بتثبيت مدير الملفات OpenIntents + المحرر الذي يقبل هاذا الفعل ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانتاج, فتح وحفض ملفات قاعدة البيانات. بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة. مهلة الحافظة مدة التخزين في الحافظة(إذا كان جهازك يدعمها) @@ -249,7 +248,7 @@ الخلفية دورات التحويل توفر الدورات الاضافية ضد هجوم توليد التركيبات ،لكنها تبطئ التحميل والحفظ. - مقدار الذاكرة (بالبايت) لاستخدامها في دالة اشتقاق المفتاح. + مقدار الذاكرة لاستخدامها في دالة اشتقاق المفتاح. درجة التوازي (عدد العمليات) لدالة اشتقاق المفتاح. مجموعات قبل نمط التحديد @@ -286,7 +285,7 @@ الخوارزمية أرقام العداد - عين كلمة المرور للمرة الواحدة + كلمة المرور للمرة الواحدة UUID من أجل <strong>حماية خصوصيتا</strong>٫<strong> إصلاح العلل</strong>٫ <strong>إضافة مميزات</strong> <strong>وجعلنا نشطاء دائما</strong>٫ نحن نعتمد على <strong>مساهمتك</strong>. خانة تأشير الملف المفتاحي @@ -351,7 +350,7 @@ قاعدة البيانات مفتوحة إعدادات الملء التلقائي حرر المدخلة - لفتح قاعدة البيانات بسرعة اربط كلمة المرور بالبصمة. + لفتح قاعدة البيانات بسرعة اربط كلمة المرور بالبصمة. لإيجاد كلمة المرور، أدخل العنوان أو اسم المستخدم أو محتوى أحد الحقول. المدخلات لإدارة معرفاتك الرقمية. \n @@ -405,7 +404,7 @@ يطالبك بتغيير المفتاح الرئيسي (بالأيام) اقترح تجديد المفتاح الرئيسي (بالأيام) اقترح التجديد - حجم التأريخ ( بالبايت) لكل مدخل + حجم التأريخ لكل مدخل الحجم الأقصى عدد عناصر التأريخ لكل مدخل أنشئ كلمة سر قوية diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index 40a1e8b8a..2977796bf 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -175,7 +175,7 @@ Forsiraj obnovu Preporučiti promenu glavnog ključa (u danima) Preporučiti obnavljanje - Ograniči veličinu istorije (u bajtovima) po unosu + Ograniči veličinu istorije po unosu Maksimalna veličina Ograniči broja stavki istorije po unosu Maksimalan broj diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..ce7d45bee --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,12 @@ + + + Обратна връзка + Алгоритъм за криптиране + Криптиране + Сигурност + Главен ключ + Добави група + Редактирай + Добави + Приемам + \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 79cd4a4aa..838ffb326 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -207,7 +207,7 @@ S\'executa l\'ordre… Grau de paral·lelisme (és a dir, nombre de fils) fets servir per la funció de derivació de la clau. Paral·lelisme - Quantitat de memòria (en bytes) usada per la funció de derivació de la clau. + Quantitat de memòria usada per la funció de derivació de la clau. Ús de la memòria Amaga els enllaços trencats en la llista de bases de dades recents Mostra la ubicació de les bases de dades recents diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3d7245aed..4975e880a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -19,12 +19,12 @@ Czech translation by Jan Vaněk --> Domovská stránka - Androidová verze správce hesel KeePass + Implementace správce hesel KeePass pro Android Přijmout Přidat záznam Přidat skupinu Šifrovací algoritmus - Časový limit aplikace + Časový limit Doba nečinnosti, po které se aplikace zamkne Aplikace Nastavení aplikace @@ -35,16 +35,16 @@ Některá zařízení nedovolují aplikacím používat schránku. Nelze vyprázdnit schránku Časový limit schránky - Doba uchování ve schránce + Doba uchování ve schránce (je-li podporována zařízením) Vyberte zkopírovat %1$s do schránky - Načítání klíče databáze… + Načítám klíč databáze… Databáze - Dešifrování obsahu databáze… + Dešifruji obsah databáze… Použít jako výchozí databázi Číslice Otevřít existující databázi Poslední přístup - Storno + Zrušit Poznámky Potvrďte heslo Vytvořeno @@ -55,27 +55,27 @@ Heslo Uložit Název - URL adresa + URL Uživatelské jméno Arcfour proudová šifra není podporována. KeePassDX nemůže zpracovat toto URI. - Soubor se nedaří vytvořit - Databázi nelze číst. + Soubor se nepodařilo vytvořit + Databázi se nepodařilo načíst. Ujistěte se, že cesta je správná. Zadejte jméno. Vyberte soubor s klíčem. Nedostatek paměti k načtení celé databáze. Je třeba zvolit alespoň jeden způsob vytváření hesla. - Zadaná hesla se neshodují. + Hesla se neshodují. Příliš vysoký „Počet průchodů“. Nastavuji na 2147483648. Je třeba, aby každý řetězec měl název kolonky. - Do nastavení „Délka“ zadejte celé kladné číslo. - Název pole - Hodnota pole + Do pole „Délka“ zadejte celé kladné číslo. + Název kolonky + Hodnota kolonky Správce souborů - Vytvoř heslo + Generovat heslo Potvrdit heslo - Vytvořené heslo + Generované heslo Název skupiny Soubor s klíčem Délka @@ -83,8 +83,8 @@ Heslo Nebylo možno načíst autentizační údaje. Nesprávný algoritmus. - Nedaří se rozpoznat formát databáze. - Soubor s klíčem je prázdný. + Nepodařilo se rozpoznat formát databáze. + Soubor klíče je prázdný. Délka Velikost položek seznamu Velikost textu v seznamu prvků @@ -97,28 +97,28 @@ Nastavení Nastavení databáze Smazat - Podpořit vývoj darem + Přispět darem Upravit Skrýt heslo Zamknout databázi Otevřít Hledat - Ukaž heslo - Jít na URL + Ukázat heslo + Přejít na URL Mínus Nikdy Žádné výsledky hledání Pro otevření tohoto URL nainstalujte webový prohlížeč. Neprohledávat položky v záloze Vynechat skupiny „Záloha“ a \"Koš\" z výsledků vyhledávání - Vytvářím novou databázi… - Zpracování… + Zakládám novou databázi… + Pracuji… Ochrana - Ke změně v databázi potřebuje KeePassDX oprávnění pro zápis. + Ke změně v databáze potřebuje KeePassDX oprávnění pro zápis. Odstranit Rijndael (AES) Kořen - Počet šifrovacích průchodů + Transformační průchody 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í. Ukládám databázi… Místo @@ -134,7 +134,7 @@ Verze %1$s 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. +\nNezapomeňte po každé úpravě zálohovat kopii svého .kdbx souboru na bezpečné místo. 5 sekund 10 sekund @@ -155,41 +155,41 @@ Šifrování Funkce pro tvorbu klíče Rozšířené ASCII - Umožnit - Databázi se nedaří načíst. - Klíč se nedaří načíst, zkuste snížit množství paměti, využívané funkcí pro tvorbu klíče. - Službu automatického vyplňování se nedaří zapnout. + Povolit + Databázi se nepodařilo načíst. + Klíč se nepodařilo načíst, zkuste snížit \"využití paměti\" pro KDF. + Službu automatického vyplňování se nepodařilo zapnout. Není možné přesunout skupinu do ní samotné. Soubor nenalezen. Zkuste jej otevřít ze správce souborů. Zobrazit uživatelská jména - V seznamech položek zobrazit uživatelská jména + V seznamech záznamů zobrazit uživatelská jména Kopie %1$s Vyplňování formulářů Zkopírovat Přesunout Vložit - Storno + Zrušit Chráněno před zápisem Čtení a zápis Chráněno před zápisem - Algoritmus šifrování databáze užitý pro všechna data. - 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í. + Algoritmus šifrování databáze použit pro všechna data. + 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í. Využití paměti - Množství paměti (v bajtech) použitých funkcí pro odvození klíče. + Množství paměti použitých funkcí pro odvození klíče. Souběžné zpracovávání - Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro vytvoření klíče. + Stupeň souběžného zpracovávání (počet vláken) použitý funkcí pro odvození klíče. Seřadit Nejnižší první ↓ Skupiny první Koš jako poslední Nadpis Uživatelské jméno - Vytvoření - Změna + Založeno + Změněno Přístup Varování - Nepoužívejte v hesle pro databázový soubor znaky mimo znakovou sadu Latin-1 (nepoužívejte znaky s diakritikou). - Pokračovat bez ochrany odemknutím heslem\? + Nepoužívejte v hesle pro databázový soubor znaky mimo znaky kódování textu (nerozpoznané znaky budou konvertovány na stejné písmeno). + Pokračovat bez ochrany odemknutí heslem\? Pokračovat bez šifrovacího klíče\? Šifrované heslo uloženo Tato databáze zatím nemá uložené heslo. @@ -198,7 +198,7 @@ Obecné Automatické vyplnění KeePassDX automatické vyplňování formulářů - Přihlásit se pomocí KeePassDX + Přihlásit se s KeePassDX Nastavit výchozí službu automatického vyplňování Povolit rychlé automatické vyplňování formulářů v ostatních aplikacích Délka generovaného hesla @@ -206,28 +206,28 @@ Znaky hesla Nastavit povolené znaky pro generátor hesel Schránka - Oznamování schránky - Ukázat oznamení schránky o kopírování pole při prohlížení záznamu + Oznámení schránky + Ukázat oznámení schránky o kopírování pole při prohlížení záznamu Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže. Zamknout Zámek obrazovky Při zhasnutí obrazovky uzamknout databázi - Pokročilé odemčení - Biometrické odemčení + Rozšířené odemknutí + Biometrické odemknutí Nechá otevřít databázi snímáním biometrického údaje Smazat šifrovací klíče - Smazat všechny šifrovací klíče související s biometrickým rozlišením + Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí Tuto funkci se nedaří spustit. V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější. - Hardware nebyl rozpoznán. + Odpovídající hardware nebyl rozpoznán. Název souboru Cesta Přiřadit hlavní klíč - Vytvořit novou databázi + Založit novou databázi Využití koše Před smazáním přesune skupiny a položky do skupiny „Koš“ - Písmo položek - Čitelnost znaků v položkách můžete přizpůsobit změnou písma + Písmo kolonek + Čitelnost znaků v kolonkách můžete přizpůsobit změnou písma Důvěřovat schránce Povolit kopírování hesla záznamu a chráněných položek do schránky 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. @@ -235,7 +235,7 @@ Popis databáze Verze databáze Text - Aplikace + Rozhraní Ostatní Klávesnice Magikeyboard @@ -253,52 +253,49 @@ Založte svůj první soubor pro správu hesel. Otevřít existující databázi Otevřete svou dříve používanou databázi ze správce souborů a pokračujte v jejím používání. - Přidejte položky do databáze - Položky pomáhají se správou vašich digitálních identit. -\n + Přidejte záznamy do databáze + Záznamy pomáhají se správou Vašich digitálních identit. +\n \nSkupiny (ekvivalent složek) organizují záznamy v databázi. - Hledejte v položkách - Zadejte název, uživatelské jméno nebo jiné položky k nalezení svých hesel. - Odemknutí databáze biometricky - Propojte své heslo s načtenou biometrikou pro rychlé odemknutí databáze. - Upravit položku - Přidejte ke své položce vlastní kolonky. Společná data mohou být sdílena mezi více různými kolonkami. + Hledat v záznamech + Zadejte název, uživatelské jméno nebo jiné kolonky k nalezení svých hesel. + Upravit záznam + 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. Vytvořit silné heslo - Vygenerujte silné heslo pro svou položku, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo. + Generujte silné heslo pro svůj záznam, definujte je podle kritérií formuláře, a nezapomeňte na bezpečné heslo. Přidat vlastní kolonky Registrovat další kolonku, zadat hodnotu a volitelně ji ochránit. Odemknout databázi Ochraňte svou databázi před zápisem - 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. -\n V režimu zápisu je možné přidávat, mazat nebo měnit všechny prvky dle libosti. - Zkopírujte kolonku - Zkopírované kolonky lze vkládat kam chcete -\n + 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 v databázi. +\nV režimu \"zápisu\" je možné přidávat, mazat nebo měnit všechny prvky podle libosti. + Zkopírovat kolonku + Zkopírované kolonky lze vkládat podle libosti. +\n \nK vyplňování formulářů použijte svou oblíbenou metodu. - Uzamkni databáze - Rychlé uzamkni databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky. + Uzamknout databázi + Rychle uzamknout databázi. Je možné nastavit, aby se databáze zamkla po určitém čase a také po zhasnutí obrazovky. Řazení položek Vyberte řazení položek a skupin. Zapojit se - Zapojte se a pomozte zvýšit stabilitu, bezpečnost a přidávání dalších funkcí. - 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. - 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> + Zapojte se a pomozte zvýšit stabilitu, bezpečnost a doplnění dalších funkcí. + 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. + 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> Tento <strong>vizuální styl</strong> je k dispozici díky vaší štědrosti. - 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> - Tato funkce je <strong>ve vývoji</strong> a potřebuje váš <strong>příspěvek</strong>, aby byla brzy k dispozici. + 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> + Tato funkce je <strong>ve vývoji</strong> a potřebuje Váš <strong>příspěvek</strong>, aby byla brzy k dispozici. Zakoupením <strong>pro</strong> varianty, <strong>Zapojením se</strong>, - 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. - Mnohé díky za vaše přispění. + 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. + Mockrát děkujeme za Váš příspěvek. Tvrdě pracujeme na brzkém vydání této funkce. - Nezapomeňte aplikaci aktualizovat instalováním nových verzí. + Pamatujte na aktualizaci aplikace instalováním nových verzí. Stáhnout - Zapojit se + Přispět ChaCha20 AES - Argon2 Vzhled aplikace Motiv vzhledu aplikace Sada ikon @@ -307,16 +304,16 @@ Magikeyboard Magikeyboard (KeePassDX) Magikeyboard nastavení - Položka + Záznam Časový limit Doba uchování položky v Magikeyboardu Informace o oznámení Zobrazit oznámení, když je položka dostupná - Položka + Záznam %1$s dostupné v Magikeyboardu %1$s Vymazat při zavření - Zavři databázi při zavření oznámení + Zavřít databázi při zavření oznámení Vzhled Vzhled klávesnice Klávesy @@ -329,37 +326,37 @@ Vymazat při ukončení Uzamknout databázi, jakmile trvání schránky vyprší nebo po uzavření oznámení Koš - Výběr položky - Při prohlížení záznamu ukázat na Magikeyboard pole položek + Výběr záznamu + Při prohlížení záznamu ukázat na Magikeyboard kolonky Smazat heslo Smaže heslo zadané po pokusu o připojení k databázi Otevřít soubor - Potomci uzlu - Přidej uzel - Přidej záznam + Podřazené prvky uzlu + Přidat uzel + Přidat záznam Přidat skupinu Informace o souboru Checkbox hesla Checkbox souboru s klíčem - Přepni ukázání hesla + Opakovat přepnutí viditelnosti hesla Ikona záznamu Generátor hesel Délka hesla - Přidej pole - Odeber pole + Přidat pole + Odebrat pole UUID Sem záznam přesunout nelze. Sem záznam zkopírovat nelze. - Ukaž počet záznamů - Ukaž počet záznamů ve skupině + Ukázat počet záznamů + Ukázat počet záznamů ve skupině Pozadí Aktualizovat - Zavři kolonky - Nelze vytvořit databázi s tímto heslem a klíčem ze souboru. - Pokročilé odemčení + Zavřít pole + Nelze vytvořit databázi s tímto heslem a souborem klíče. + Rozšířené odemknutí Biometrika - Automaticky otevřít biometrickou pobídku - Automaticky žádat biometriku, je-li databáze nastavena k jejímu použití + Automaticky otevřít pobídku + Automaticky žádat rozšířené odemknutí, je-li databáze nastavena k jejímu použití Zapnout Vypnout Hlavní klíč @@ -375,7 +372,7 @@ OTP Neplatná OTP tajnost. Nejméně jeden přihlašovací údaj musí být zadán. - Sem skupinu kopírovat nemůžete. + Sem skupinu kopírovat nelze. Tajný klíč musí mít formát Base32. Čítač musít být mezi %1$d a %2$d. Interval musít být mezi %1$d a %2$d vteřinami. @@ -387,14 +384,14 @@ Databáze obsahuje duplikátní UUID. Opravit chybu založením nového UUID pro duplikáty a pokračovat\? Databáze otevřena - Kopírujte pole záznamů pomocí schránky Vašeho zařízení - K snadnějšímu otevření databáze použijte pokročilé odemknutí + Kopírovat kolonky záznamů pomocí schránky svého zařízení + K snadnějšímu otevření databáze použijte rozšířené odemknutí Komprese dat Komprese dat snižuje velikost databáze Maximální počet Omezit počet položek v historii záznamu Maximální velikost - Omezit velikost historie na záznam (v bajtech) + Omezit velikost historie na záznam Doporučit změnu Dporučit změnu hlavního klíče (dny) Vynutit změnu @@ -416,20 +413,20 @@ Skupina Koš Uložit databázi automaticky Uložit databázi po každé důležité akci (v režimu \"Zápis\") - Připojené soubory + Přílohy Obnovit historii Smazat historii Akce auto-klávesy - Akce klávesy \"Jít\" po stisknutí klávesy \"Položka\" + Akce klávesy \"Jít\" po stisknutí klávesy \"Kolonka\" Stáhnout %1$s Zahajuji… Probíhá: %1$d%% Dokončuji… Kompletní! Skrýt propadlé záznamy - Propadlé záznamy nejsou ukázány + Propadlé záznamy nebudou ukázány Kontakt - Příspěvky + Příspění Feedback Snadné hledání Při otevření databáze žádat hledání @@ -439,23 +436,23 @@ Uchová informaci o tom, kde jsou uloženy soubory s klíči Ukázat nedávné soubory Ukázat umístění nedávných databází - Skrýt špatné odkazy na databáze - Skrýt nesprávné odkazy v seznamu nedávných databází + Skrýt chybné odkazy na databáze + Skrýt chybné odkazy v seznamu nedávných databází Udělit právo zápisu pro uložení změn v databázi 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. 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>. - Nedaří se vytvořit soubor s databází. + Nepodařilo se vytvořit soubor databáze. Přidat přílohu - Zahodit - Zahodit změny\? - Ověřit + Zavrhnout + Zavrhnout změny\? + Zkontrolovat Nastavit správu One-Time hesla (HOTP / TOTP) pro založení tokenu požadovaného pro dvoufázové ověření (2FA). Nastavit OTP Automaticky navrhnout výsledky hledání z webové domény nebo ID aplikace Samočinné hledání - Ukáže tlačítko zámku v uživatelském rozhraní - Ukázat tlačítko zámku + Zobrazí tlačítko zámku v uživatelském rozhraní + Zobrazit tlačítko zámku Nastavení samovyplnění Přístup k souboru zrušenému správcem souborů Tento štítek již existuje. @@ -472,14 +469,14 @@ Hledat v subdoméně Tento text se s požadovanou položkou neshoduje. Přidat položku - Automaticky přepnout na předchozí klávesnici po provedení Akce auto-klávesy + Automaticky přepnout na předchozí klávesnici po provedení \"Akce auto-klávesy\" Akce auto-klávesy Automaticky přepnout zpět na předchozí klávesnici na obrazovce ověřovacích údajů databáze Obrazovka ověřovacích údajů databáze Přepnout klávesnici Nahrát %1$s - Info o údajích - Databáze KeePassu předpokládá uchovávat jen malé pomocné sobory (např. PGP soubory). + Informace o údajích + 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. Nahráním tohoto souboru nahradíte existující soubor. @@ -493,4 +490,67 @@ Odstraní přílohy obsažené v databázi, ale nikoli přílohy propojené se záznamem Přidat přílohu Nahrát přílohu k záznamu pro uložení důležitých externích dat. + Ukáže UUID propojené se záznamem + Ukázat UUID + Uložení dat není povoleno, je-li databáze v režimu pouze pro čtení. + Zeptat se na uložení dat, jakmile byl formulář přezkoušen + Zeptat se před uložením + Pokuste se uložit údaje hledání, když manuálně vybíráte položku + Uložit info hledání + Zavřít databázi po samovyplnění polí + Zavřít databázi + Po uzamknutí databáze automaticky přepnout zpět na předchozí klávesnici + Uzamknout databázi + Pokuste se uložit sdílené info, když manuálné vybíráte položku + Uložit sdílené info + Oznámení + Vyžadována aktualizace biometrického zabezpečení. + Žádné přihlašovací ani biometrické údaje nejsou registrovány. + Trvale odstranit všechny uzly z koše\? + Režim registrace + Režim ukládání + Režim vyhledávání + Jméno kolonky již existuje. + Uložení nové položky v režimu databáze pouze pro čtení není povoleno + Enter + Backspace + Vybrat záznam + Zpět na předchozí klávesnici + Vlastní kolonky + Smazat všechny šifrovací klíče související s rozpoznáním rozšířeného odemknutí\? + Dovolí pro otevření databáze použít heslo Vašeho zařízení + Odemknutí heslem zařízení + Heslo zařízení + Zadejte heslo a klikněte na tlačítko \"Rozšířené odemknutí\". + Nelze inicializovat pobídku pro rozšířené odemknutí. + Chyba při rozšířeném odemknutí: %1$s + Otisk pro rozšířené odemknutí nebyl rozpoznán + Nelze načíst klíč rozšířeného odemknutí. Prosím, smažte jej a opakujte proces rozpoznání odemknutí. + Načíst důvěrný údaj pomocí dat rozšířeného odemknutí + Otevřít databázi pomocí rozpoznání rozšířeného odemknutí + Varování: Pokud použijete rozpoznání rozšířeného odemknutí, musíte si i nadále pamatovat hlavní heslo. + Rozpoznání rozšířeného odemknutí + Pro uložení důvěrných údajů otevřete pobídku rozšířeného odemknutí + Pro odemknutí databáze otevřete pobídku rozšířeného odemknutí + Smazat klíč rozšířeného odemknutí + Rozšířené odemknutí databáze + Časový limit rozšířeného odemknutí + Trvání použití rozšířeného odemknutí než bude obsah téhož smazán + Za účelem rozšířeného odemknutí neukládat žádný šifrovaný obsah + Přechodné rozšířené odemknutí + Pro odstranění klíčů rozšířeného odemknutí klepnout + Argon2id + Argon2d + Abyste rychle odemknuli databázi, propojte své heslo s naskenovanou biometrikou nebo údaji zámku zařízení. + Vypršení pokročilého odemknutí + Obsah + Seznam nelze řádně sestavit. + URI databáze nelze načíst. + Návrhy samovyplnění přidány. + Návrhy inline + Pokusí ze zobrazit návrhy samovyplnění přímo z kompatibilní klávesnice + Přístup k souboru odebrán správcem souborů, uzavřete databázi a nově ji otevřete z jejího adresáře. + Databázi nově načíst + Přepsat externí změny uložením databáze nebo databázi včetně posledních změn nově načíst. + Informace obsažená ve Vašem databázovém souboru by změněna mimo aplikaci. \ No newline at end of file diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2482668f9..77911db5e 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -35,7 +35,7 @@ Nogle enheder, vil ikke lade programmer bruge udklipsholderen. Kunne ikke rydde udklipsholderen Udklipsholder timeout - Varighed af opbevaring i udklipsholder + Varighed af opbevaring i udklipsholder (hvis det understøttes) Vælg for at kopiere %1$s til udklipsholder Opretter databasenøgle… Database @@ -174,7 +174,7 @@ Databasekrypteringsalgoritme anvendt for alle data. For at generere nøglen til krypteringsalgoritmen, omdannes hovednøglen ved hjælp af en tilfældigt saltet nøgleafledningsfunktion. Hukommelsesforbrug - Hukommelse (i bytes), som anvendes af nøgleafledningsfunktion. + Hukommelse, som anvendes af nøgleafledningsfunktion. Parallelitet Grad af parallelitet (dvs. antallet af tråde), som anvendes af nøgleafledningsfunktion. Sorter @@ -206,7 +206,7 @@ Angiv tilladte tegn for adgangskodegenerator Udklipsholder Udklipsholdermeddelelser - Aktivér udklipsholder for at kopiere felter når en post vises + Vis udklipsholder for at kopiere felter når en post vises Hvis automatisk sletning af udklipsholder mislykkes, slet historikken manuelt. Lås Skærmlås @@ -217,7 +217,7 @@ Slet krypteringsnøgler Slet alle krypteringsnøgler, der er relateret til biometrisk genkendelse Funktionen kunne ikke startes. - Android-version %1$s opfylder ikke minimum versionskrav %2$s. + Enheden kører Android %1$s, men har brug for %2$s eller nyere. Kunne ikke finde den tilsvarende hardware. Filnavn Sti @@ -240,13 +240,13 @@ Magikeyboard Aktiver et brugerdefineret tastatur, der udfylder adgangskoder og alle identitetsfelter Tillad ingen hovednøgle - Aktiver knappen \"Åbn\", hvis der ikke er valgt nogen legitimationsoplysninger + Tillader at trykke på knappen \"Åbn\", hvis der ikke er valgt nogen legitimationsoplysninger Skrivebeskyttet Åbn database skrivebeskyttet som standard Pædagogiske tips Fremhæv elementer for at lære, hvordan programmet fungerer Nulstil pædagogiske tip - Vis alle pædagogiske info igen + Vis al uddannelsesinformation igen Nulstilling af pædagogiske tips Opret databasefilen Opret den første adgangskodeadministrationsfil. @@ -258,8 +258,6 @@ \nGrupper (~mapper) organiserer poster i databasen. Søg i poster Indtast titel, brugernavn eller indhold af andre felter for at hente adgangskoder. - Biometrisk oplåsning af databasen - Knyt adgangskoden til det scannede biometri for hurtigt at låse databasen op. Rediger posten Rediger post med brugerdefinerede felter. Pool data kan refereres mellem forskellige indtastningsfelter. Opret en stærk adgangskode @@ -297,7 +295,6 @@ Bidrag ChaCha20 AES - Argon2 Tema Tema, der bruges i programmet Ikonpakke @@ -394,7 +391,7 @@ Max. antal Begræns antallet af historikposter pr. indtastning Max. størrelse - Begræns historikstørrelsen (i bytes) pr. post + Begræns historikstørrelsen pr. post Anbefalet fornyelse Anbefal ændring af hovednøglen (dage) Gennemtving fornyelse @@ -436,10 +433,10 @@ For at <strong>holde vores frihed</strong>, <strong>rette fejl</strong>, <strong>tilføje funktioner</strong> og <strong>at være altid aktiv</strong>, regner vi med <strong>bidrag</strong>. Hurtig søgning Anmod om en søgning når en database åbnes - Gem placering af databaser - Husk placeringen af databaser - Gem placering af nøglefiler - Husker placeringen af databasernøglefiler + Husk placeringer af databaser + Holder styr på, hvor databaserne gemmes + Husk placering af nøglefiler + Holder styr på, hvor nøglefiler gemmes Vis seneste filer Vis placeringer af de seneste databaser Skjule brudte databaselinks @@ -452,7 +449,7 @@ Kassér Kasser ændringer\? Valider - Foreslår automatisk søgeresultater fra webdomænet eller applikationsId + Foreslår automatisk søgeresultater fra webdomænet eller applikations-id Automatisk søgning Viser låseknappen i brugergrænsefladen Vis låseknap @@ -467,10 +464,10 @@ Blokeringsliste for webdomæne Blokeringsliste der forhindrer automatisk udfyldning af programmer Blokeringsliste for program - Skift automatisk tilbage til det forrige tastatur efter udførelse af automatisk tastehandling + Skift automatisk tilbage til det forrige tastatur efter udførelse af \"Automatisk tastehandling\" Auto nøglehandling Skift automatisk tilbage til det forrige tastatur på databasens legitimationsskærm - Database legitimationsoplysninger skærm + Skærmbilledet til databaselegitimationsoplysninger Skifte tastatur Filter Søg på webdomæner med begrænsninger på underdomæner @@ -494,7 +491,6 @@ Fjerner vedhæftede filer indeholdt i databasen, men ikke knyttet til en post Fjern ikke-sammenkædede data Data - Kryptoobjektet kunne ikke hentes. Biometrisk sikkerhedsopdatering påkrævet. Indholdet af nøglefilen bør aldrig ændres og bør i bedste fald indeholde tilfældigt genererede data. Det anbefales ikke at tilføje en tom nøglefil. @@ -511,4 +507,9 @@ Søgetilstand Det er ikke tilladt at gemme et nyt element i en skrivebeskyttet database Oplysninger om legitimationsoplysninger + Der er ikke tilmeldt biometriske legitimationsoplysninger eller enhedsoplysninger. + Overfør en vedhæftet fil til posten for at gemme vigtige eksterne data. + Forsøger at gemme delte oplysninger, når der foretages et manuelt indtastningsvalg + Forsøger at gemme delte oplysninger, når der foretages et manuelt indtastningsvalg + Feltnavnet findes allerede. \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4ae49bf53..fd44ec980 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -22,7 +22,7 @@ Translations from David Ramiro --> Kontakt - Beitrag + Beiträge Rückmeldung Webseite Android-Implementierung des Passwortmanagers KeePass @@ -43,9 +43,9 @@ Zwischenablage-Timeout Dauer der Speicherung in der Zwischenablage (falls vom Gerät unterstützt) %1$s in die Zwischenablage kopieren - Datenbank-Schlüsseldatei erzeugen… + Datenbank-Schlüsseldatei erzeugen … Datenbank - Entschlüsselung der Datenbankinhalte… + Entschlüsselung der Datenbankinhalte … Als Standard-Datenbank verwenden Zahlen KeePassDX © %1$d Kunzisoft ist <strong>quelloffen</strong> und <strong>ohne Werbung</strong>.\nDie Nutzung der Software erfolgt auf eigene Verantwortung und ohne jegliche Garantie. Die Applikation wird unter den Bedingungen der <strong>GPLv3</strong> lizenziert. @@ -97,7 +97,7 @@ Länge Größe der Listenelemente Schriftgröße der Listenelemente - Datenbank wird geladen… + Datenbank wird geladen … Kleinbuchstaben Passwort verstecken Passwörter standardmäßig mit (***) maskieren @@ -118,12 +118,12 @@ Nie Keine Suchergebnisse Bitte einen Webbrowser installieren, um diese URL zu öffnen. - Papierkorb/Backup nicht durchsuchen - Die Gruppen „Backup“ und „Papierkorb“ werden bei der Suche nicht berücksichtigt + Recycle bin und Backup nicht durchsuchen + Die Gruppen „Backup“ und „Recycle bin“ werden bei der Suche nicht berücksichtigt Schnellsuche Beim Öffnen einer Datenbank eine Suche veranlassen - Neue Datenbank anlegen… - Ausführen… + Neue Datenbank anlegen … + Ausführen … Sicherheit Schreibgeschützt KeePassDX benötigt Schreibrechte, um etwas an der Datenbank zu ändern. @@ -132,7 +132,7 @@ Start Schlüsseltransformationen Zusätzliche Schlüsseltransformationen bieten einen besseren Schutz gegen Wörterbuch- oder Brute-Force-Angriffe. Allerdings dauert dann auch das Laden und Speichern der Datenbank entsprechend länger. - Datenbank wird gespeichert… + Datenbank wird gespeichert … Leerzeichen Suchen Natürliche Sortierung @@ -199,7 +199,7 @@ Verschlüsselungsalgorithmus der Datenbank wird für sämtliche Daten verwendet. Um den Schlüssel für den Verschlüsselungsalgorithmus zu generieren, wird der Hauptschlüssel umgewandelt, wobei ein zufälliger Salt in der Schlüsselberechnung verwendet wird. Speichernutzung - Größe des Speichers (in Bytes) der für die Schlüsselableitung genutzt wird. + Größe des Speichers der für die Schlüsselableitung genutzt wird. Parallelismus Grad des Parallelismus (d. h. Anzahl der Threads), der für die Schlüsselableitung genutzt wird. Sortieren @@ -218,7 +218,7 @@ Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen Zwischenablage Verschlüsselungsschlüssel löschen - Alle mit der biometrischen Erkennung verbundenen Verschlüsselungsschlüssel löschen. + Lösche alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig. Keine entsprechende Hardware. Papierkorb-Nutzung @@ -231,7 +231,7 @@ Datenbankbeschreibung Datenbankversion Text - App + Interface Andere Tastatur Magikeyboard @@ -249,8 +249,6 @@ \nGruppen (wie Ordner) helfen, Einträge in der Datenbank zu ordnen. Einträge durchsuchen Titel, Benutzernamen oder Inhalte anderer Feldern eingeben, um die Passwörter wiederzufinden. - Biometrische Datenbank-Entsperrung - Verknüpft Ihr Passwort mit Ihrer gescannten Biometrie, um Ihre Datenbank schnell zu entsperren. Eintrag bearbeiten Bearbeiten Sie Ihren Eintrag mit persönlichen Feldern. Persönliche Felder können mit Querverweisen aus anderen Einträgen ergänzt werden. Ein starkes Passwort erstellen @@ -269,7 +267,7 @@ Mitmachen Mithelfen, um Stabilität und Sicherheit zu verbessern und weitere Funktionen zu ermöglichen. Anders als viele andere Passwortmanager ist dieser <strong>werbefrei</strong>, <strong>quelloffen</strong> und unter einer <strong>Copyleft-Lizenz</strong>. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro). - Mit dem Kauf der Pro-Version erhält man Zugang zu diesem <strong>visuellen Stil</strong> und unterstützt insbesondere <strong>die Umsetzung gemeinschaftlicher Projektarbeiten.</strong> + Mit dem Kauf der Pro-Version erhalten Sie Zugriff auf diesen <strong>visuellen Stil</strong> und unterstützen insbesondere <strong>die Umsetzung gemeinschaftlicher Projekte.</strong> Dieser <strong>visuelle Stil</strong> wurde wegen Ihrer Großzügigkeit freigeschaltet. Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren <strong>Beitrag.</strong> Diese Funktion ist <strong>in Entwicklung</strong> und erfordert <strong>Ihren Beitrag</strong>, um bald verfügbar zu sein. @@ -278,12 +276,11 @@ Sie ermutigen die Entwickler:innen, <strong>neue Funktionen</strong> einzuführen und gemäß Ihren Anmerkungen <strong>Fehler zu beheben</strong>. Vielen Dank für Ihre Unterstützung. Wir bemühen uns, diese Funktion bald zu veröffentlichen. - Vergessen Sie nicht, Ihre App aktuell zu halten, indem Sie neue Versionen installieren. + Denken Sie daran, Ihre App auf dem neuesten Stand zu halten, indem Sie neue Versionen installieren. Download Unterstützen ChaCha20 AES - Argon2 Symbolpaket In der App verwendetes Symbolpaket Eine Gruppe kann nicht in sich selbst verschoben werden. @@ -302,10 +299,10 @@ Schreibgeschützt Schreibschutz der Datenbank aktivieren Datenbank standardmäßig schreibgeschützt öffnen - Den Öffnungsmodus für die Sitzung ändern. -\n -\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank. -\nMit „Änderbar“ können alle Elemente hinzugefügt, gelöscht oder geändert werden. + Den Öffnungsmodus für die Sitzung ändern. +\n +\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank. +\nMit „Änderbar“ können Sie alle Elemente nach Belieben hinzufügen, löschen oder ändern. Eintrag bearbeiten Datenbank kann nicht geladen werden. Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern. @@ -339,7 +336,7 @@ Speicherort zuletzt verwendeter Datenbanken anzeigen Defekte Datenbankverknüpfungen ausblenden Nicht funktionierende Verknüpfungen in der Liste der zuletzt verwendeten Datenbanken ausblenden - Nicht die App beenden… + App nicht beenden … Datenbank sperren, wenn auf dem Hauptbildschirm der Zurück-Button gedrückt wird Beim Schließen löschen Papierkorb @@ -350,7 +347,7 @@ Datei öffnen Eintrag hinzufügen Gruppe hinzufügen - Datei-Info + Dateiinformationen Symbol für den Eintrag Passwort-Generator Passwortlänge @@ -376,8 +373,8 @@ Biometrisch Aktivieren Deaktivieren - Biometrische Abfrage automatisch öffnen - Automatisch nach Biometrie fragen, wenn die Datenbank dafür eingerichtet ist + Abfrage automatisch öffnen + Automatisch nach der erweiterten Entsperrung fragen, wenn die Datenbank dafür eingerichtet ist Hauptschlüssel Sicherheit Verlauf @@ -397,7 +394,7 @@ Zeitraum muss zwischen %1$d und %2$d Sekunden liegen. Token muss %1$d bis %2$d Stellen enthalten. %1$s mit derselben UUID %2$s existiert bereits. - Datenbank wird erstellt… + Datenbank wird erstellt … Sicherheits-Einstellungen Hauptschlüssel-Einstellungen Die Datenbank enthält UUID-Duplikate. @@ -410,7 +407,7 @@ Maximale Anzahl Anzahl der Verlaufseinträge pro Eintrag begrenzen Maximale Größe - Verlaufsumfang (in Bytes) pro Eintrag begrenzen + Verlaufsumfang pro Eintrag begrenzen Erneuerung empfehlen (Nach Tagen) Änderung des Hauptschlüssels empfehlen Erneuerung erzwingen @@ -426,21 +423,21 @@ Die Datenbank konnte nicht gespeichert werden. Datenbank speichern Papierkorb leeren - Befehl ausführen… + Befehl ausführen … Sollen die ausgewählten Knoten wirklich gelöscht werden\? Der Schlüsselspeicher ist nicht richtig initialisiert. Papierkorb-Gruppe Datenbank automatisch speichern - Automatisches Speichern der Datenbank nach einer wichtigen Aktion (im Modus \"Bearbeiten\") + Automatisches Speichern der Datenbank nach einer wichtigen Aktion (im Modus „Bearbeiten“) Anhänge Historie wiederherstellen Historie löschen Auto-Key-Aktion Aktion der Go-Taste, die automatisch nach dem Drücken einer Feldtaste ausgeführt wird %1$s herunterladen - Initialisieren… + Initialisieren … Fortschritt: %1$d%% - Fertigstellen… + Fertigstellen … Vollständig! Abgelaufene Einträge ausblenden Abgelaufene Einträge werden nicht angezeigt @@ -474,9 +471,9 @@ Gemeinsame Infos durchsuchen Starten Sie die Anwendung, die das Formular enthält, neu, um die Sperrung zu aktivieren. Automatisches Ausfüllen sperren - Liste der Domains, auf denen ein automatisches Ausfüllen unterbunden wird + Liste der Domains, auf denen ein automatisches Ausfüllen unterlassen wird Liste gesperrter Web-Domains - Liste der Apps, in denen ein automatisches Ausfüllen unterbunden wird + Liste der Apps, in denen ein automatisches Ausfüllen verhindert wird Liste gesperrter Anwendungen Suche Web-Domains mit Subdomain-Beschränkungen Subdomain-Suche @@ -486,7 +483,7 @@ Tastatur wechseln Auto-Key-Aktion Datenbank-Anmeldebildschirm - Automatisches Zurückschalten auf die vorherige Tastatur nach Ausführung der automatischen Tastenaktion + Nach Ausführung der automatischen Tastenaktion automatisch zur vorherigen Tastatur wechseln Automatisches Zurückschalten zur vorherigen Tastatur auf dem Datenbank-Anmeldebildschirm Laden Sie einen Anhang für Ihren Eintrag hoch, um wichtige externe Daten zu speichern. Anmeldeinformationen @@ -503,7 +500,7 @@ %1$s hochladen Anhang hinzufügen Entfernt Anhänge die in der Datenbank enthalten, aber keinem Eintrag zugeordnet sind - Die Datei trotzdem hinzufügen\? + Soll die Datei trotzdem hinzugefügt werden\? Zeigt die mit einem Eintrag verknüpfte UUID an UUID anzeigen Das Speichern von Daten ist für eine als schreibgeschützt geöffnete Datenbank nicht zulässig. @@ -511,7 +508,6 @@ Automatisches Zurückschalten zur vorherigen Tastatur nach dem Sperren der Datenbank Datenbank sperren Benachrichtigung - Kryptoobjekt kann nicht abgerufen werden. Biometrisches Sicherheitsupdate erforderlich. Es sind keine biometrischen oder Geräteanmeldeinformationen registriert. Registrierungsmodus @@ -526,4 +522,40 @@ Versuche bei manueller Eingabeauswahl gemeinsam genutzte Informationen zu speichern Gemeinsam genutzte Informationen speichern Sollen alle ausgewählten Knoten wirklich aus dem Papierkorb gelöscht werden\? + Der Feldname existiert bereits. + Warnung: Sie müssen sich immer noch an ihr Masterpasswort erinnern, wenn sie die erweiterte Entsperrerkennung verwenden. + Öffne den erweiterten Entsperrdialog zum Speichern von Anmeldeinformationen + Öffnen des erweiterten Entsperrdialogs zum Entsperren der Datenbank + Löschen des Schlüssels zum erweiterten Entsperren + Erweiterte Entsperrerkennung + Ihr Passwort verbinden mit Ihrem gescannten biometrischen oder berechtigen Gerät um schnell Ihre Datenbank zu entsperren. + Erweiterte Entsperrung der Datenbank + Verfallzeit der erweiterten Entsperrung + Dauer der erweiterten Entsperrung bevor sein Inhalt gelöscht wird + Verfall der erweiterten Entsperrung + Keinen verschlüsselten Inhalt speichern, um erweiterte Entsperrung zu benutzen + Temporäre erweiterte Entsperrung + Erlaubt Ihn die Geräteanmeldedaten zum Öffnen der Datenbank zu verwenden + Drücken um erweiterte Entsperrschlüssel zu löschen + Inhalt + Öffne Datenbank mit erweiterter Entsperrerkennung + Eingabetaste + Rücktaste + Wähle Eintrag + Zurück zur vorherigen Tastatur + Benutzerdefinierte Felder + Alle Verschlüsselungsschlüssel, die mit der erweiterten Entsperrerkennung zusammenhängen, löschen\? + Geräteanmeldedaten entsperren + Geräteanmeldedaten + Geben sie das Passwort ein, und dann klicken sie den \"Erweitertes Entsperren\" Knopf. + Initialisieren des erweitertes Entsperren Dialogs fehlgeschlagen. + Erweitertes Entsperren Fehler: %1$s + Konnte den Abdruck des erweiterten Entsperrens nicht erkennen + Kann den Schlüssel zum erweiterten Entsperren nicht lesen. Bitte löschen sie ihn und wiederholen sie Prozedur zum Erkennen des Entsperrens. + Extrahiere Datenbankanmeldedaten mit Daten aus erweitertem Entsperren + Argon2id + Argon2d + Die Liste kann nicht ordnungsgemäß neu erstellt werden. + Datenbank-URI kann nicht abgerufen werden. + Datenbank neu laden \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index dc947505b..7dee954af 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -165,7 +165,7 @@ Αλγόριθμος κρυπτογράφησης βάσης δεδομένων που χρησιμοποιείται για όλα τα δεδομένα. Για να δημιουργηθεί το κλειδί για τον αλγόριθμο κρυπτογράφησης, το κύριο κλειδί μετασχηματίζεται χρησιμοποιώντας μια τυχαία αλατισμένη λειτουργία εξαγωγής κλειδιών. Χρήση μνήμης - Ποσότητα μνήμης (σε bytes) που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών. + Ποσότητα μνήμης που θα χρησιμοποιηθεί από τη λειτουργία εξαγωγής κλειδιών. Παραλληλισμός Βαθμός παραλληλισμού (δηλ. Αριθμός νημάτων) που χρησιμοποιείται από τη συνάρτηση εξαγωγής κλειδιών. Ταξινόμηση @@ -213,7 +213,7 @@ Περιγραφή Βάσης Δεδομένων Έκδοση Βάσης Δεδομένων Κείμενο - Εφαρμογή + Διεπαφή Άλλα Πληκτρολόγιο Magikeyboard @@ -257,12 +257,11 @@ ενθαρρύνετε τους προγραμματιστές να δημιουργούν <strong>νέες λειτουργίες</strong> και να <strong>διορθώνουν σφάλματα</strong> σύμφωνα με τις παρατηρήσεις σας. Ευχαριστούμε πολύ για τη συνεισφορά σας. Εργαζόμαστε σκληρά για να διαθέσουμε αυτό το χαρακτηριστικό γρήγορα. - Μην ξεχνάτε να ενημερώνετε την εφαρμογή σας, εγκαθιστώντας νέες εκδόσεις. + Θυμηθείτε να ενημερώνετε την εφαρμογή σας, εγκαθιστώντας νέες εκδόσεις. Download Συνεισφορά ChaCha20 AES - Argon2 Θέμα Εφαρμογής Θέμα που χρησιμοποιείται στην εφαρμογή Πακέτο Εικονιδίων @@ -286,7 +285,7 @@ Αλλάξτε τη λειτουργία ανοίγματος για το session. \n \nΤο \"Προστατευμένο από εγγραφή\" αποτρέπει τυχόν μη επιθυμητές αλλαγές στη βάση δεδομένων. -\nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία. +\nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία όπως επιθυμείτε. Επεξεργασία καταχώρησης Δεν ήταν δυνατή η φόρτωση της βάσης δεδομένων σας. Δεν ήταν δυνατή η φόρτωση του κλειδιού. Προσπαθήστε να μειώσετε την KDF \"Χρήση μνήμης\". @@ -315,7 +314,7 @@ Λειτουργία επιλογής Μη κλείσιμο της εφαρμογής … Πατήστε \'Πίσω\' για να κλειδώσετε - Κλείδωμα της βάσης δεδομένων όταν ο χρήστης κάνει κλικ στο κουμπί \"πίσω\" στη αρχική οθόνη + Κλείδωμα της βάσης δεδομένων όταν ο χρήστης κάνει κλικ στο κουμπί \"πίσω\" στην οθόνη προέλευσης Καθαρισμός στο κλείσιμο Κλείδωμα της βάσης δεδομένων όταν λήξει η διάρκεια του προχείρου ή η ειδοποίηση κλείσει αφού αρχίσετε να την χρησιμοποιείτε Κάδος ανακύκλωσης @@ -353,10 +352,10 @@ Προηγμένο ξεκλείδωμα Βιομετρικό ξεκλείδωμα Σας επιτρέπει να σαρώσετε το βιομετρικό σας για να ανοίξετε τη βάση δεδομένων - Αυτόματο άνοιγμα βιομετρικής προτροπής - Ζητήστε αυτόματα βιομετρία εάν η βάση δεδομένων έχει ρυθμιστεί για να τη χρησιμοποιήσει + Αυτόματο άνοιγμα προτροπής + Ζητήστε αυτόματα προηγμένο ξεκλείδωμα εάν η βάση δεδομένων έχει ρυθμιστεί για να το χρησιμοποιήσει Διαγράψτε τα κλειδιά κρυπτογράφησης - Διαγράψτε όλα τα κλειδιά κρυπτογράφησης που σχετίζονται με τη βιομετρική αναγνώριση + Διαγράψτε όλα τα κλειδιά κρυπτογράφησης που σχετίζονται με το προηγμένο ξεκλείδωμα Ενεργοποίηση Απενεργοποίηση Κύριο κλειδί @@ -393,7 +392,7 @@ Μέγιστος αριθμός Περιορίστε τον αριθμό των στοιχείων ιστορικού ανά καταχώριση Μέγιστο μέγεθος - Περιορίστε το μέγεθος ιστορικού (σε bytes) ανά καταχώριση + Περιορίστε το μέγεθος ιστορικού ανά καταχώριση Συστήστε ανανέωση Προτεινόμενη αλλαγή του κύριου κλειδιού (ημέρες) Εξαναγκαστική ανανέωση @@ -407,8 +406,6 @@ Gzip Ενεργοποιώντας ένα προσαρμοσμένο πληκτρολόγιο συγκεντρώνει τους κωδικούς πρόσβασής σας και όλα τα πεδία ταυτότητας Ρυθμίσεις πληκτρολογίου συσκευής - Βιομετρικό ξεκλείδωμα βάσης δεδομένων - Συνδέστε τον κωδικό πρόσβασής σας στο σαρωμένο βιομετρικό σας για να ξεκλειδώσετε γρήγορα τη βάση δεδομένων σας. Δεν ήταν δυνατή η αποθήκευση της βάσης δεδομένων. Αποθήκευση βάσης δεδομένων Αδειάστε τον κάδο ανακύκλωσης @@ -468,7 +465,7 @@ Λίστα αποκλεισμού Εφαρμογών Λίστα αποκλεισμού που αποτρέπει την αυτόματη συμπλήρωση εφαρμογών Αυτόματη ενέργεια πλήκτρου - Επιστρέψτε αυτόματα στο προηγούμενο πληκτρολόγιο μετά την εκτέλεση της ενέργειας του αυτόματου πλήκτρου + Επιστρέψτε αυτόματα στο προηγούμενο πληκτρολόγιο μετά την εκτέλεση της ενέργειας του \"Αυτόματου πλήκτρου\" Επιστρέψτε αυτόματα στο προηγούμενο πληκτρολόγιο στην οθόνη διαπιστευτηρίων βάσης δεδομένων Οθόνη διαπιστευτηρίων βάσης δεδομένων Εναλλαγή πληκτρολογίου @@ -506,7 +503,6 @@ Προσπαθήστε να αποθηκεύσετε κοινόχρηστες πληροφορίες όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης Αποθήκευση κοινόχρηστων πληροφοριών Ειδοποίηση - Δεν είναι δυνατή η ανάκτηση κρυπτογραφικού αντικειμένου. Απαιτείται ενημέρωση βιομετρικής ασφάλειας. Κανένα πιστοποιητικό βιομετρίας ή συσκευής δεν είναι εγγεγραμμένο. Να διαγραφούν οριστικά όλοι οι κόμβοι από τον κάδο ανακύκλωσης; @@ -514,4 +510,46 @@ Λειτουργία αποθήκευσης Λειτουργία αναζήτησης Η αποθήκευση ενός νέου αντικειμένου δεν επιτρέπεται σε μια βάση δεδομένων μόνο για ανάγνωση + Το όνομα πεδίου υπάρχει ήδη. + Προηγμένο ξεκλείδωμα αναγνώρισης + Προειδοποίηση: Θα πρέπει να θυμάστε τον κύριο κωδικό πρόσβασης εάν χρησιμοποιείτε προηγμένο ξεκλείδωμα αναγνώρισης. + Ανοίξτε τη βάση δεδομένων με προηγμένο ξεκλείδωμα αναγνώρισης + Ανοίξτε τη προηγμένη προτροπή ξεκλειδώματος για αποθήκευση διαπιστευτηρίων + Ανοίξτε τη προηγμένη προτροπή ξεκλειδώματος για να ξεκλείιδώσετε τη βάση δεδομένων + Διαγραφή προηγμένου κλειδιού ξεκλειδώματος + Enter + Backspace + Επιλέξτε καταχώριση + Επιστροφή στο προηγούμενο πληκτρολόγιο + Προσαρμοσμένα πεδία + Διαγραφή όλων των κλειδιών κρυπτογράφησης που σχετίζονται με το προηγμένο ξεκλείδωμα αναγνώρισης; + Σας επιτρέπει να χρησιμοποιήσετε τα διαπιστευτήρια της συσκευής σας για να ανοίξετε τη βάση δεδομένων + Ξεκλείδωμα διαπιστευτηρίων συσκευής + Διαπιστευτήρια συσκευής + Πληκτρολογήστε τον κωδικό πρόσβασης, και στη συνέχεια κάντε κλικ στο κουμπί \"Προηγμένο ξεκλείδωμα\". + Δεν είναι δυνατή η προετοιμασία προτροπής προηγμένου ξεκλειδώματος. + Δεν ήταν δυνατή η αναγνώριση αποτυπώματος προηγμένου ξεκλειδώματος + Προηγμένο ξεκλείδωμα σφάλμα: %1$s + Δεν είναι δυνατή η ανάγνωση του προηγμένου κλειδιού ξεκλειδώματος. Διαγράψτε το και επαναλάβετε τη διαδικασία αναγνώρισης ξεκλειδώματος. + Εξαγωγή διαπιστευτηρίων βάσης δεδομένων με προηγμένο ξεκλείδωμα δεδομένων + Συνδέστε τον κωδικό πρόσβασής σας με το σαρωμένο βιομετρικό ή τα διαπιστευτήρια της συσκευής σας για να ξεκλειδώσετε γρήγορα τη βάση δεδομένων σας. + Προηγμένο ξεκλείδωμα βάσης δεδομένων + Χρονικό όριο προηγμένου ξεκλειδώματος + Προσωρινό προηγμένο ξεκλείδωμα + Μην αποθηκεύετε κανένα κρυπτογραφημένο περιεχόμενο για να χρησιμοποιήσετε προηγμένο ξεκλείδωμα + Διάρκεια της χρήσης προηγμένου ξεκλειδώματος πριν την διαγραφή του περιεχομένου + Λήξη προηγμένου ξεκλειδώματος + Πατήστε για διαγραφή προηγμένων κλειδιών ξεκλειδώματος + Περιεχόμενα + Argon2id + Argon2d + Δεν είναι δυνατή η σωστή αναδημιουργία της λίστας. + Δεν είναι δυνατή η ανάκτηση του URI βάσης δεδομένων. + Προστέθηκαν προτάσεις αυτόματης συμπλήρωσης. + Απόπειρα εμφάνισης προτάσεων αυτόματης συμπλήρωσης απευθείας από ένα συμβατό πληκτρολόγιο + Προτάσεις στην ίδια γραμμή + Πρόσβαση στο αρχείο που ανακλήθηκε από το διαχειριστή αρχείων, κλείστε τη βάση δεδομένων και ανοίξτε το ξανά από τη θέση του. + Αντικαταστήστε τις εξωτερικές τροποποιήσεις αποθηκεύοντας τη βάση δεδομένων ή φορτώστε την ξανά με τις πιο πρόσφατες αλλαγές. + Οι πληροφορίες που περιέχονται στο αρχείο της βάσης δεδομένων σας έχουν τροποποιηθεί εκτός της εφαρμογής. + Επαναφόρτωση βάσης δεδομένων \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 71fd512da..2bfed9ce2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -19,9 +19,9 @@ Spanish translation by José I. Paños. Updated by David García-Abad (23-09-2013) --> - Commentario + Comentarios Página de inicio - Implementación para Android del gestor de contraseñas KeePass + Implementación en Android del gestor de contraseñas KeePass Aceptar Añadir entrada Añadir grupo @@ -31,30 +31,30 @@ Aplicación Configuración de la aplicación Paréntesis - Explora ficheros con OpenIntents File Manager + Se requiere un administrador de archivos que acepte la acciones de intención ACTION_CREATE_DOCUMENT y ACTION_OPEN_DOCUMENT para crear, abrir y guardar los archivos de la base de datos. Portapapeles limpiado Portapapeles caducado - Duración del almacenamiento en el portapapeles (si lo admite el dispositivo) + Duración del almacenamiento en el portapapeles (si lo admite su dispositivo) Seleccionar para copiar %1$s en el portapapeles - Creando clave de la base de datos… + Recuperando la clave de la base de datos… Base de datos Descifrando el contenido de la base de datos… - Utilice como base de datos por defecto + Utilizar como base de datos por defecto Dígitos - KeePassDX © %1$d Kunzisoft es \u0020de <strong>código abierto</strong> y <strong>sin publicidad</strong>. + KeePassDX © %1$d Kunzisoft es de <strong>código abierto</strong> y <strong>sin publicidad</strong>. \nSe proporciona tal cual, bajo licencia <strong>GPLv3</strong>, sin ninguna garantía. Abrir base de datos existente - Acceso + Accedido Cancelar Notas Confirmar contraseña - Creación - Caducidad + Creado + Caduca Archivo de clave - Modificación + Modificada Contraseña Guardar - Nombre + Título URL Nombre de usuario No se admite el cifrador de flujo Arcfour. @@ -64,11 +64,11 @@ Asegúrese de que la ruta sea correcta. Proporcione un nombre. Seleccione un archivo de clave. - No queda memoria para cargar toda la base de datos. + No hay memoria para cargar toda la base de datos. Debe seleccionar al menos un tipo de generación de contraseñas. Las contraseñas no coinciden. Pasadas demasiado grande. Establecido a 2147483648. - Proporcione un número entero positivo en el campo «Longitud». + Introduzca un número entero positivo en el campo \"Longitud\". Explorador de archivos Generar contraseña Confirmar contraseña @@ -78,7 +78,7 @@ Longitud Contraseña Contraseña - Contraseña o archivo de clave no válido. + No se pudieron leer las credenciales. No se pudo reconocer el formato de la base de datos. Longitud Tamaño de los elementos de la lista @@ -103,7 +103,7 @@ Menos Nunca Sin resultado de búsqueda - Instale un navegador web para abrir este URL. + Instale un navegador web para abrir esta URL. No buscar en las entradas de respaldo Omite los grupos «Respaldo» y «Papelera de reciclaje» de los resultados de búsqueda Creando nueva base de datos… @@ -124,7 +124,9 @@ No se admite esta versión de la base de datos. Mayúsculas Versión %1$s - Introduzca una contraseña y/o un archivo de clave para desbloquear su base de datos. + Introduzca la contraseña y/o el archivo clave para desbloquear su base de datos. +\n +\nHaga una copia de seguridad de su archivo de base de datos en un lugar seguro después de cada cambio. 5 segundos 10 segundos @@ -146,28 +148,28 @@ ASCII extendido Permitir Error del portapapeles - Algunos dispositivos Android tienen un error en la implementación del portapapeles que provoca fallos al copiar desde las aplicaciones. - Falló la limpieza del portapapeles + Algunos dispositivos no permiten que las aplicaciones usen el portapapeles. + No se pudo limpiar el portapapeles Cada cadena debe tener un nombre de campo. El servicio de autocompletado no se ha podido habilitar. Nombre del campo Valor del campo No se pudo encontrar el archivo. Intente volver a abrirlo en el explorador de archivos. - El algoritmo es incorrecto. + Algoritmo incorrecto. El archivo de clave está vacío. Copia de %1$s Llenado de formulario Protección Protegida contra escritura - KeePassDX necesita permiso de escritura para modificar la base de datos. - Algoritmo de cifrado utilizado en todos los datos. + Dependiendo del administrador de archivos, puede que KeePassDX no permita escribir en su almacenamiento. + Algoritmo de cifrado utilizado para todos los datos. Para generar la clave del algoritmo de cifrado, la clave maestra se transforma mediante una función de derivación de claves con una sal aleatoria. Uso de memoria - Cantidad de memoria (en bytes) que usará la función de derivación de clave. + Cantidad de memoria que usará la función de derivación de clave. Paralelismo Grado de paralelismo (p. ej. número de hilos) usados por la función de derivación de clave. Ordenar - Inferiores primero ↓ + Más bajo primero ↓ Agrupar antes Papelera debajo Título @@ -177,8 +179,8 @@ Acceso Resultados de búsqueda Atención - Datos de entrada no encontrados. - Evite emplear en la base de datos caracteres que no pertenezcan al formato de codificación del texto (los caracteres no reconocidos se convierten a la misma letra). + No se pudieron encontrar los datos de entrada. + Evite los caracteres de la contraseña fuera del formato de codificación de texto en el archivo de la base de datos (los caracteres no reconocidos se convierten a la misma letra). ¿Continuar sin la protección de desbloqueo de contraseña\? ¿Continuar sin clave de cifrado\? Contraseña cifrada almacenada @@ -197,25 +199,25 @@ Establecer los caracteres permitidos del generador de contraseñas Portapapeles Notificaciones del portapapeles - Mostrar notificaciones del portapapeles para copiar campos al examinar una entrada + Mostrar las notificaciones del portapapeles para copiar campos al examinar una entrada Bloquear Bloqueo de pantalla Bloquear la base de datos cuando la pantalla esté apagada Desbloqueo avanzado Desbloqueo biométrico - Le permite escanear su huella dactilar u otro dato biométrico para abrir la base de datos + Le permite escanear sus datos biométricos para abrir la base de datos Eliminar claves de cifrado - Eliminar todas las claves de cifrado relacionadas con el reconocimiento biométrico + Eliminar todas las claves de cifrado relacionadas con el reconocimiento de desbloqueo avanzado No se pudo iniciar esta funcionalidad. - La versión de Android del dispositivo es %1$s, pero es necesaria la %2$s o posterior. - No se encontró el hardware correspondiente. + El dispositivo funciona con Android %1$s, pero necesita %2$s o posterior. + No se pudo encontrar el hardware correspondiente. Nombre del archivo Ruta Asignar una clave maestra - Crear base de datos nueva + Crear una base de datos nueva Uso de la papelera de reciclaje Mueve los grupos y las entradas al grupo \"Papelera de reciclaje\" antes de eliminarlos - Fuente de los campos + Tipografía del campo Cambiar la fuente de los campos para una mejor visibilidad del carácter Portapapeles de confianza Permitir copiar la contraseña de entrada y los campos protegidos al portapapeles @@ -223,11 +225,11 @@ Descripción de la base de datos Versión de la base de datos Texto - Aplicación + Interfaz Otro Teclado Teclado mágico - Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente. + Active un teclado personalizado que llene sus contraseñas y todos los campos de identidad fácilmente Restablecer sugerencias didácticas Mostrar de nuevo toda la información didáctica Se restablecieron las sugerencias didácticas @@ -241,8 +243,6 @@ \nLos grupos (~carpetas) organizan las entradas en su base de datos. Busque registros fácilmente Busque entradas por título, nombre de usuario u otros campos para recuperar fácilmente sus contraseñas. - Desbloquee su base de datos con su huella digital - Vincule la contraseña a su escaneo biométrico para desbloquear rápidamente la base de datos. Editar la entrada Edite la entrada con campos personalizados, puede agregar referencias a los datos de la agrupación entre campos de diferentes entradas. Crear una contraseña segura @@ -261,29 +261,28 @@ Participar Participe para aumentar la estabilidad, la seguridad y agregar más funciones. A diferencia de muchas aplicaciones de administración de contraseñas, esta es <strong>sin publicidad</strong>, <strong>software libre con copyleft</strong> y no recopila datos personales en sus servidores, sin importar la versión que use. - Al comprar la versión pro, tendrá acceso a <strong>la característica visual </strong>y usted ayudará especialmente a <strong>la realización de proyectos comunitarios.</strong> - Esta <strong>característica visual </strong>está disponible gracias a tu generosidad. + Al comprar la versión pro, tendrá acceso al <strong>estilo visual </strong>y ayudará especialmente a <strong>la realización de proyectos comunitarios.</strong> + Este <strong>estilo visual </strong>está disponible gracias a tu generosidad. Para mantener nuestra libertad y estar siempre vigente, contamos con tu <strong>contribución.</strong> Esta función está <strong>en desarrollo</strong> y requiere de tu <strong>contribución</strong> para estar disponible dentro de poco. Al comprar la versión <strong>pro</strong>, Al <strong>contribuir</strong>, - usted alienta a los desarrolladores a crear <strong>nuevas funciones</strong> y a <strong>errores de configuración</strong> de acuerdo con tus comentarios. + anima a los desarrolladores a crear <strong>nuevas funciones</strong> y a <strong>corregir errores</strong> de acuerdo con sus comentarios. Muchas gracias por tu contribución. Estamos trabajando duro para lanzar esta característica rápidamente. - No olvide mantener su aplicación actualizada. + Recuerde mantener su aplicación actualizada instalando nuevas versiones. Descargar Contribuir ChaCha20 - AES-KDF - Argon2 - Tema de aplicación + AES + Tema de la aplicación Tema utilizado en la aplicación Seleccione un paquete de iconos Cambiar el paquete de iconos en la aplicación Editar entrada No se pudo cargar la base de datos. No se pudo cargar la clave. Intente disminuir el uso de memoria de KDF. - No se puede mover un grupo dentro de sí mismo. + No puede mover un grupo dentro de sí mismo. Enseña nombres de usuario Enseña nombres de usuador en las listras de entradas Copiar @@ -294,13 +293,13 @@ Modificable Compilación %1$s Si la eliminación del cortapapeles falla, elimine su historial manualmente. - ATENCIÓN: todas las aplicaciones comparten el portapapeles. Si copia datos confidenciales, otros programas podrían recuperarlos. + Advertencia: El portapapeles es compartido por todas las aplicaciones. Si se copian datos sensibles, otros programas pueden recuperarlos. No permitir claves maestras - Activar el botón «Abrir» si no se selecciona ninguna credencial - Pantallas didácticas - Resaltar los elementos para conocer cómo funciona la aplicación + Permite pulsar el botón \"Abrir\" si no se seleccionan credenciales + Sugerencias educativas + Destacar elementos para aprender cómo funciona la aplicación Protegida contra escritura - Abrir su base de datos protegida contra escritura de manera predeterminada + Abrir la base de datos de solo lectura por defecto Proteja la base de datos contra escritura Teclado mágico Teclado mágico (KeePassDX) @@ -318,56 +317,56 @@ Apariencia Tema del teclado Teclas - Vibrar al pulsar - Emitir sonido al pulsar + Vibrar al pulsar tecla + Sonar al pulsar tecla Modo de selección No cierre la aplicación… - Bloquear la base de datos cuando se pulsa el botón Atrás en la pantalla inicial + Bloquear la base de datos cuando el usuario pulse el botón atrás en la pantalla inicial Vaciar al cerrar - Bloquear la base de datos cuando la duración del portapapeles expire o la notificación sea cerrada + Bloquear la base de datos cuando expire la duración del portapapeles o cuando se cierre la notificación después de empezar a utilizarla Papelera de reciclaje Selección de entrada Mostrar campos de entrada en el Teclado mágico al ver una entrada Eliminar contraseña - Elimina la contraseña proporcionada tras un intento de conexión + Elimina la contraseña introducida tras un intento de conexión a una bse de datos Abrir archivo - Elementos secundarios del nodo + Nodos hijo Añadir nodo Añadir entrada Añadir grupo - Información de archivo - Casilla de contraseña - Casilla de archivo de clave + Información del archivo + Casilla de verificación de la contraseña + Casilla de verificación del archivo de clave Icono de entrada Generador de contraseñas Longitud de contraseña Añadir campo - Eliminar campo + Eliminar el campo UUID - No se puede mover una entrada aquí. - No se puede copiar una entrada aquí. - Mostrar cantidad de entradas - Mostrar la cantidad de entradas en un grupo + No puede mover una entrada aquí. + No puede copiar una entrada aquí. + Mostrar el número de entradas + Mostrar el número de entradas en un grupo Fondo Actualizar Cerrar campos No se puede crear la base de datos con esta contraseña y este archivo de clave. Desbloqueo avanzado Biometría - Abrir petición de datos biométricos automáticamente - Abrir automáticamente la petición de datos biométricos cuando se define una clave biométrica para una base de datos + Abrir petición automáticamente + Solicitar automáticamente el desbloqueo avanzado si la base de datos está configurada para utilizarlo Activar Desactivar Cambiar el modo de apertura de la sesión. \n \n\"Protegido contra escritura\" evita cambios no deseados en la base de datos. -\n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos. - Presione hacia atrás en la raíz para bloquear +\n\"Modificable\" le permite agregar, eliminar o modificar todos los elementos como desee. + Presione \'Atrás\' para bloquear Repetir la visibilidad de la contraseña - Llave maestra + Clave maestra Seguridad Historial - Configuración de contraseña de un solo uso + Establecer una contraseña de un solo uso Tipo de contraseña de un solo uso Secreto Período (segundos) @@ -380,16 +379,16 @@ Clave secreta debe estar en formato Base32. Contador debe estar entre %1$d y %2$d. No se puede guardar la base de datos. - Este texto no encaja con la información requerida. + Este texto no coincide con el elemento requerido. No fue posible crear el archivo de base de datos. - Parar lograr <strong>mantener nuestra libertad</strong>, <strong>corregir errores</strong>, <strong>agregar características</strong> y <strong>siempre estar activos</strong>, contamos con tu <strong>contribución</strong>. + Parar lograr <strong>mantener nuestra libertad</strong>, <strong>corregir errores</strong>, <strong>agregar características</strong> y <strong>estar siempre activos</strong>, contamos con tu <strong>contribución</strong>. Añadir elemento - Descarga completa! + ¡Completado! Finalizando… En progreso: %1$d%% Inicializando… Descargar %1$s - Guardar la base de datos tras acciones importantes (en modo \"Modificable\") + Guardar la base de datos después de cada acción importante (en modo \"Modificable\") Guardar base de datos automáticamente Bloquear autocompletado Cambiar teclado @@ -398,12 +397,12 @@ Ninguna Compresión Nombre de usuario predeterminado - Requerir cambiar la contraseña maestra la próxima vez (una sola vez) + Requerir cambiar la contraseña maestra la próxima vez (una vez) Forzar renovación la próxima vez - Requerir un cambio de contraseña maestra (días) + Requerir un cambio de la contraseña maestra (días) Forzar renovación Tamaño máximo - Usar desbloqueo avanzado para abrir una base de datos de manera más fácil + Usar el desbloqueo avanzado para abrir una base de datos más fácilmente Muestra el botón de bloqueo en la interfaz Mostrar botón de bloqueo Configuración de autocompletado @@ -412,8 +411,8 @@ Otorga acceso de escritura para guardar cambios en la base de datos Mostrar ubicaciones de bases de datos recientes Mostrar archivos recientes - Recordar la ubicación de las bases de datos - Guardar ubicaciones de bases de datos + Lleva un registro de los lugares donde se almacenan las bases de datos + Recordar las ubicaciones de las bases de datos La base de datos contiene UUIDs duplicados. Restaurar historial Vaciar papelera de reciclaje @@ -422,12 +421,12 @@ %1$s con la misma UUID %2$s ya existe. Esta etiqueta ya existe. Descartar - ¿Descartar cambios\? + ¿Descartar los cambios\? Validar Contribución - Contactar - Período debe estar entre %1$d y %2$d segundos. - No se puede copiar un grupo aquí. + Contacto + El período debe estar entre %1$d y %2$d segundos. + No puede copiar un grupo aquí. La compresión de datos reduce el tamaño de la base de datos Compresión de datos No se recomienda agregar un archivo de claves vacío. @@ -436,19 +435,19 @@ Al cargar este archivo reemplazará el existente. ¿Borrar los nodos seleccionados de forma permanente\? El acceso al archivo fue revocado por el administrador de archivos - Ejecutando el comando … + Ejecutando el comando… Ocultar enlaces rotos en la lista de bases de datos recientes Ocultar enlaces de bases de datos rotos Realiza un seguimiento de dónde se almacenan los archivos de claves - Recuerdar las ubicaciones de los archivos de claves + Recordar las ubicaciones de los archivos de clave Buscar dominios web con restricciones de subdominios Búsqueda de subdominio Solicite una búsqueda al abrir una base de datos Búsqueda rápida - Borrar historial - El token debe tener de %1$d a %2$d dígitos. + Eliminar el historial + El token debe contener de %1$d a %2$d dígitos. Archivos adjuntos - Adjuntar + Añadir el archivo adjunto Información de credenciales Base de datos abierta Adjuntar @@ -458,4 +457,94 @@ Se supone que una base de datos KeePass contiene solo pequeños archivos de utilidad (como archivos de clave PGP). \n \nLa base de datos puede volverse muy grande y reducir su rendimiento con esta subida. + Grupo de la papelera de reciclaje + Filtrar + Elimina los archivos adjuntos contenidos en la base de datos pero no vinculados a una entrada + Eliminar los datos no vinculados + Datos + ¿Borrar todas las claves de encriptación relacionadas con el reconocimiento de desbloqueo avanzado\? + Tiempo límite de desbloqueo avanzado + Duración del uso de desbloqueo avanzado antes de borrar su contenido + Expiración de desbloqueo avanzado + No almacenar ningún contenido encriptado para utilizar el desbloqueo avanzado + Desbloqueo avanzado temporal + Le permite usar la credenciales de su dispositivo para abrir la base de datos + Desbloqueo de las credenciales del dispositivo + Toque para eliminar las teclas de desbloqueo avanzadas + Contenido + Copiar los campos de entrada usando el portapapeles de su dispositivo + Credenciales del dispositivo + Introduzca la contraseña y luego haga clic en el botón \"Desbloqueo avanzado\". + No se pudo inicializar el indicador de desbloqueo avanzado. + Error de desbloqueo avanzado: %1$s + No se pudo reconocer la impresión de desbloqueo avanzado + No se pudo leer la llave de desbloqueo avanzada. Por favor, bórrela y repita el procedimiento de reconocimiento de desbloqueo. + Extraer la credencial de la base de datos con datos de desbloqueo avanzado + Abrir la base de datos con reconocimiento de desbloqueo avanzado + Advertencia: Aún debes recordar tu contraseña maestra si usas el reconocimiento de desbloqueo avanzado. + Reconocimiento de desbloqueo avanzado + Abrir el indicador de desbloqueo avanzado para almacenar las credenciales + Abrir el aviso de desbloqueo avanzado para desbloquear la base de datos + El almacén de claves no está debidamente inicializado. + Se requiere una actualización de la seguridad biométrica. + No se ha inscrito ninguna credencial biométrica o del dispositivo. + El contenido del archivo clave nunca debe modificarse y, en el mejor de los casos, debe contener datos generados al azar. + ¿Borrar permanentemente todos los nodos de la papelera de reciclaje\? + Modo de registro + Modo de guardado + Modo de búsqueda + ¿Resolver el problema generando nuevos UUID para que los duplicados continúen\? + Borrar la clave de desbloqueo avanzado + El nombre del campo ya existe. + Guardar un nuevo elemento no está permitido en una base de datos de sólo lectura + Recomendar la renovación + Limitar el tamaño del historial por entrada + Limitar el número de elementos del historial por entrada + Número máximo + Configuración del teclado del dispositivo + Color personalizado de la base de datos + Recomendar cambiar la contraseña maestra (días) + Notificación + Ocultar las entradas expiradas + Buscar información compartida + Argon2id + Argon2d + Subir %1$s + Configurar la gestión de contraseñas de una sola vez (HOTP / TOTP) para generar un token solicitado para la autenticación de dos factores (2FA). + Establecer la contaseña de un solo uso + Vincule su contraseña con su credencial biométrica o del dispositivo escaneada para desbloquear rápidamente su base de datos. + Desbloqueo avanzado de la base de datos + No se permite guardar datos en una base de datos abierta como de sólo lectura. + Reiniciar la aplicación que contiene el formulario para activar el bloqueo. + Lista de bloqueo que impide el llenado automático de los dominios web + Lista de bloqueo de los dominios web + Lista de bloqueo que impide el llenado automático de las aplicaciones + Lista de bloqueo de las aplicaciones + Pedir que se guarden los datos cuando se valide un formulario + Pedir que se guarden los datos + Intente guardar la información de la búsqueda cuando haga una selección de entrada manual + Guardar la información de la búsqueda + Sugerir automáticamente los resultados de la búsqueda desde el dominio web o el ID de la aplicación + Búsqueda automática + Cerrar la base de datos después de una selección de autocompletado + Cerrar la base de datos + Entrar + Retroceder + Seleccionar la entrada + Volver al teclado anterior + Campos personalizados + Cambiar automáticamente al teclado anterior después de bloquear la base de datos + Bloquear la base de datos + Cambiar automáticamente al teclado anterior después de ejecutar \"Acción de la tecla automática\" + Acción de la tecla automática + Cambiar automáticamente al teclado anterior en la pantalla de credenciales de la base de datos + Pantalla de credenciales de la base de datos + Acción de la tecla automática + Intentar guardar la información compartida cuando hagas una selección de entrada manual + Guardar información compartida + Buscar automáticamente la información compartida para llenar el teclado + Muestra el UUID vinculado a una entrada + Mostrar UUID + No es posible reconstruir adecuadamente la lista. + La URI de la base de datos no puede ser recuperada. \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e2902554e..8121e9be1 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -37,7 +37,7 @@ در حال ذخیره پایگاه داده درجه موازی سازی (یعنی تعداد موضوعات) که توسط عملکرد مشتق کلیدی استفاده می شود. موازی کاری - مقدار حافظه (در بایت) که توسط تابع مشتق کلید مورد استفاده قرار گیرد. + مقدار حافظه که توسط تابع مشتق کلید مورد استفاده قرار گیرد. استفاده از حافظه دورهای رمزگذاری اضافی محافظت بالاتری در برابر حملات نیروی وحشی ایجاد می کنند ، اما در واقع می توانند سرعت و بارگذاری را کاهش دهند. دور تحول diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 82c8be006..95957ab83 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -30,7 +30,7 @@ Ohjelman asetukset Hakasulkeet Tietokantojen avaamista, luomista ja tallentamista varten tarvitaan tiedostonhallintaohjelma, joka tukee ACTION_CREATE_DOCUMENT ja ACTION_OPEN_DOCUMENT Intent-toimintoja. - Leikepöytä tyhjennetty. + Leikepöytä tyhjennetty Leikepöytävirhe Jotkin laitteet eivät anna sovellusten käyttää leikepöytää. Leikepöytää ei voitu tyhjentää @@ -71,7 +71,7 @@ Salasanat eivät täsmää. Kierroksia on liian paljon. Asetetaan se arvoon 2147483648. Jokaisella tekstillä tulee olla kentässä nimi. - Syötä positiivinen kokonaisluku pituus-kenttään + Syötä positiivinen kokonaisluku pituus-kenttään. Kentän nimi Kentän arvo Tiedostoselain @@ -161,7 +161,7 @@ Turvallisuus Muokkaa merkintää Ainakin yksi pääsytieto tulee olla asetettuna. - Avainta ei pystytty lataamaan. Kokeile vähentää KDF \"Muistin käyttöä\". + Avainta ei pystytty lataamaan. Kokeile vähentää KDF ”Muistin käyttöä”. Tietokantaa ei pystytty avaamaan. Virheellinen OTP salaisuus. OTP @@ -232,7 +232,7 @@ Luota leikepöytään Vaihda fontti, jota käytetään kentissä parantaaksesi merkkien näkyvyyttä Kenttäfontti - Siirrä ryhmät ja tietueet \"Roskakori\" ryhmään ennen poistamista + Siirrä ryhmät ja tietueet ”Roskakori” ryhmään ennen poistamista Roskakorin käyttö Aseta pääavain Polku @@ -249,7 +249,7 @@ Salasanamerkit Asettaa oletuspituuden generoiduille salasanoille Rinnakkaisuus - Avaimen johtamisfunktion käyttämän muistin (tavuina) määrä. + Avaimen johtamisfunktion käyttämän muistin määrä. Muistin käyttö Pääavain muunnetaan käyttäen satunnaista suolattua avaimen johtamisfunktiota, jotta salausalgoritmin avain voidaan generoida. Salasanatietokannan salausalgoritmi, jota käytetään kaikelle datalle. @@ -324,4 +324,4 @@ Et voi siirtää ryhmää itsensä sisälle. Automaattista täyttöä ei voitu ottaa käyttöön. Solmun lapset - + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9561da61c..0275a145c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -33,7 +33,7 @@ Remplissage de formulaire Parenthèses (ou autres) ASCII étendu - Un gestionnaire de fichiers qui accepte l\'action d\'intention ACTION_CREATE_DOCUMENT et ACTION_OPEN_DOCUMENT est nécessaire pour créer, ouvrir et enregistrer des fichiers de base de données. + Un gestionnaire de fichiers qui accepte l’action d’intention ACTION_CREATE_DOCUMENT et ACTION_OPEN_DOCUMENT est nécessaire pour créer, ouvrir et enregistrer des fichiers de base de données. Autoriser Presse-papier vidé Erreur de presse-papier @@ -133,7 +133,7 @@ Tours de transformation Des tours de chiffrement supplémentaires fournissent une protection plus élevée contre les attaques par force brute, mais cela peut considérablement ralentir les opérations de chargement et d’enregistrement. Utilisation de la mémoire - Quantité de mémoire (en octets) à utiliser par la fonction de dérivation de clé. + Quantité de mémoire à utiliser par la fonction de dérivation de clé. Parallélisme Degré de parallélisme (nombre de fils d’exécution) utilisé par la fonction de dérivation de clé. Enregistrement de la base de données… @@ -205,7 +205,7 @@ Description de la base de données Version de la base de données Texte - Application + Interface Autres Clavier Magiclavier @@ -225,8 +225,6 @@ \nLes groupes (≈dossiers) organisent les entrées dans votre base de données. Rechercher dans les entrées Saisir le titre, le nom d’utilisateur ou le contenu des autres champs pour récupérer vos mots de passe. - Déverrouillage biométrique de la base de données - Associe votre mot de passe à votre empreinte biométrique numérisée pour déverrouiller rapidement votre base de données. Modifier l’entrée Modifie votre entrée avec des champs personnalisés. La collection des données peut être référencée entre différents champs de l’entrée. Créer un mot de passe fort @@ -264,7 +262,6 @@ Twofish ChaCha20 AES - Argon2 5 secondes 10 secondes @@ -299,7 +296,7 @@ Déplacer Coller Annuler - Autoriser l\'absence de clé principale + Autoriser l’absence de clé principale Autorise l’appui du bouton « Ouvrir » si aucun identifiant n’est sélectionné Protéger en écriture Modifiable @@ -309,7 +306,7 @@ Changez le mode d’ouverture pour la session. \n \n« Protégé en écriture » empêche les modifications involontaires de la base de données. -\n« Modifiable » vous permet d’ajouter, de supprimer ou de modifier tous les éléments. +\n« Modifiable » vous permet d’ajouter, de supprimer ou de modifier tous les éléments comme vous le souhaitez. Modifier l’entrée Impossible de charger votre base de données. Impossible de charger la clé. Veuillez essayer de diminuer l’utilisation mémoire de la fonction de dérivation de clé. @@ -336,9 +333,9 @@ Appui clavier audible Changement de clavier Écran des identifications de la base de données - Revenir automatiquement au clavier précédent sur l\'écran des identifications de la base de données + Revenir automatiquement au clavier précédent sur l’écran des identifications de la base de données Action de touche automatique - Revenir automatiquement au clavier précédent après avoir exécuté \"Action de touche automatique\" + Revenir automatiquement au clavier précédent après avoir exécuté « Action de touche automatique » Mode sélection Veuillez ne pas tuer l’application… Appuyer sur « Retour » pour verrouiller @@ -378,12 +375,12 @@ Désactiver Biométrie Ouvrir automatiquement l’invite biométrique - Demande automatiquement la reconnaissance biométrique si la base de données est configurée pour l\'utiliser + Demande automatiquement la reconnaissance biométrique si la base de données est configurée pour l’utiliser Clé principale Sécurité Historique Configuration d’un mot de passe à usage unique - Type OTP + Type MDP à usage unique Secret Période (secondes) Compteur @@ -409,9 +406,9 @@ Compression de données La compression des données réduit la taille de la base de données Nombre maximum - Limite le nombre d\'éléments de l’historique par entrée + Limite le nombre d’éléments de l’historique par entrée Taille maximum - Limite la taille de l’historique (en octets) par entrée + Limite la taille de l’historique par entrée Recommander le renouvellement Recommande le changement de la clé principale (jours) Forcer le renouvellement @@ -434,8 +431,8 @@ Enregistrement automatique de la base de données Enregistre la base de données après chaque action importante (en mode « Modifiable ») Attachements - Restaurer l\'historique - Effacer l\'historique + Restaurer l’historique + Effacer l’historique Action de touche automatique Action de la touche « Go » après avoir appuyé sur une touche « Champ » Téléchargement %1$s @@ -449,7 +446,7 @@ Contribution Afin de <strong>garder notre liberté</strong>, <strong>corriger les bugs</strong>, <strong>ajouter des fonctionnalités</strong> et <strong>être toujours actif</strong>, nous comptons sur votre <strong>contribution</strong>. Recherche rapide - Demande une recherche lors de l\'ouverture d\'une base de données + Demande une recherche lors de l’ouverture d’une base de données Mémoriser l’emplacement des bases de données Garde en mémoire l’emplacement où les bases de données sont stockées Mémoriser les emplacements des fichiers clé @@ -466,25 +463,25 @@ Abandonner Abandonner les modifications \? Valider - Suggère automatiquement des résultats de recherche à partir du domaine Web ou de l\'application ID + Suggérer automatiquement des résultats de recherche à partir du domaine Web ou de l’identifiant de l’application Recherche automatique - Affiche le bouton de verrouillage dans l\'interface utilisateur + Affiche le bouton de verrouillage dans l’interface utilisateur Afficher le bouton de verrouillage Paramètres de remplissage automatique Accès au fichier révoqué par le gestionnaire de fichiers Ce label existe déjà. - Redémarrez l\'application contenant le formulaire pour activer le blocage. + Redémarrez l’application contenant le formulaire pour activer le blocage. Blocker le remplissage automatique Liste de blocage qui empêche le remplissage automatique des domaines Web Liste de blocage de domaine Web Liste de blocage qui empêche le remplissage automatique des applications - Liste de blocage d\'application + Liste de blocage d’application Recherche automatiquement les informations partagées pour remplir le clavier Rechercher les informations partagées Filtre Recherche des domaines Web avec des contraintes de sous-domaines Recherche de sous-domaine - Ce texte ne correspond pas à l\'élément demandé. + Ce texte ne correspond pas à l’élément demandé. Ajouter un élément Téléverser %1$s Téléverse une pièce-jointe à votre entrée pour enregistrer d’importantes données externes. @@ -497,30 +494,71 @@ Informations des identifiants Supprimer les données non-liées peut réduire la taille de votre base de données mais peut également supprimer des données utilisées par des extensions KeePass. Supprimer ces données quand même ? - Il n\'est pas recommandé d\'ajouter un fichier clé vide. + Il n’est pas recommandé d’ajouter un fichier clé vide. Le contenu du fichier clé ne devrait jamais changer, et dans le meilleur des cas, devrait contenir des données générées aléatoirement. Supprimer les données non-liées Supprimer les pièces jointes contenues dans la base de données mais non-liées à une entrée Données - Affiche l\'UUID lié à une entrée - Afficher l\'UUID - L\'enregistrement des données n\'est pas autorisé pour une base de données ouverte en lecture seule. - Demande à enregistrer des données lorsqu\'un formulaire est validé + Affiche l’UUID lié à une entrée + Afficher l’UUID + L’enregistrement des données n’est pas autorisé pour une base de données ouverte en lecture seule. + Demande à enregistrer des données quand un formulaire est validé Demander à enregistrer des données - Essaie d\'enregistrer les informations de recherche lors de la sélection manuelle d\'une entrée + Essayer d’enregistrer les informations de recherche lors de la sélection manuelle d’une entrée Enregistrer les infos de recherche Ferme la base de données après une sélection de remplissage automatique Fermer la base de données Revient automatiquement au clavier précédent après le verrouillage de la base de données Verrouiller la base de données - Essaie d\'enregistrer les informations partagées lors de la sélection manuelle d\'une entrée + Essayer d’enregistrer les informations partagées lors de la sélection manuelle d’une entrée Enregistrer les infos partagées Notification - Impossible de récupérer l\'objet crypto. Mise à jour de sécurité biométrique requise. Supprimer définitivement tous les nœuds de la corbeille \? Mode enregistrement Mode sauvegarde Mode recherche - L\'enregistrement d\'un nouvel élément n\'est pas autorisé dans une base de données en lecture seule + L’enregistrement d’un nouvel élément n’est pas autorisé dans une base de données en lecture seule + Le nom du champ existe déjà. + Supprimer toutes les clés de chiffrement liées à la reconnaissance de déverrouillage avancée \? + Vous permet d\'utiliser les informations d\'identification de votre appareil pour ouvrir la base de données + Déverrouillage par identifiants de l\'appareil + Déverouillage de l\'appareil + Tapez le mot de passe, puis cliquez sur le bouton \"Déverrouillage avancé\". + Impossible d\'initialiser l\'invite de déverrouillage avancé. + Erreur de déverrouillage avancé : %1$s + Impossible de reconnaître l\'empreinte de déverrouillage avancé + Impossible de lire la clé de déverrouillage avancé. Veuillez la supprimer et répéter la procédure de reconnaissance de déverrouillage. + Extraire les identifiants de la base de données avec des données de déverrouillage avancées + Ouvrir la base de données avec la reconnaissance de déverrouillage avancée + Attention : Vous devez toujours vous souvenir de votre mot de passe principal si vous utilisez la reconnaissance de déverrouillage avancée. + Reconnaissance de déverrouillage avancée + Ouvrez l\'invite de déverrouillage avancé pour stocker les informations d\'identification + Ouvrez l\'invite de déverrouillage avancé pour déverrouiller la base de données + Supprimer la clé de déverrouillage avancé + Entrer + Retour arrière + Sélection d\'une entrée + Retour au clavier précédent + Champs customisés + Lier votre mot de passe à vos informations d\'identification biométriques ou de périphérique scannées pour déverrouiller rapidement votre base de données. + Déverrouillage avancé de la base de données + Délai du déverrouillage avancé + Durée d\'utilisation du déverrouillage avancé avant de supprimer son contenu + Expiration du déverrouillage avancé + Ne stocker aucun contenu crypté pour utiliser le déverrouillage avancé + Déverrouillage avancé temporaire + Appuyez pour supprimer les clés de déverrouillage avancées + Contenu + Argon2id + Argon2d + Impossible de reconstruire correctement la liste. + L\'URI de la base de données ne peut pas être récupéré. + Suggestions de remplissage automatique ajoutées. + Tente d\'afficher des suggestions de remplissage automatique directement à partir d\'un clavier compatible + Suggestions en ligne + Écraser les modifications externes en sauvegardant la base de données ou recharger-la avec les dernières modifications. + Accès au dossier révoqué par le gestionnaire de fichiers, fermer la base de données et la rouvrir à partir de son emplacement. + Les informations contenues dans votre fichier de base de données ont été modifiées en dehors de l\'application. + Recharger la base de données \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index f631a1f9a..d5db3826d 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -27,7 +27,7 @@ Algoritmo de cifrado Función de derivación de chave Tempo de espera da aplicación - Tempo antes de bloquear a base de datos cando a aplicación está inactiva. + Tiempo de inactividad antes de bloquear la base de datos Aplicación Parénteses ASCII extendido diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index aba004a6f..ada9f8ff9 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -175,7 +175,7 @@ Trajno izbrisati odabrane čvorove\? Verzija %1$s Izgradnja %1$s - Šifrirana lozinka pohranjena + Šifrirana lozinka spremljena Povijest Izgled Opće @@ -195,7 +195,7 @@ Biometrijsko otključavanje Otvaranje baze podataka skeniranjem biometrike Izbriši ključeve šifriranja - Izbriši sve ključeve šifriranja povezane s biometrijskim prepoznavanjem + Izbriši sve ključeve šifriranja povezane s naprednim prepoznavanjem otključavanja Nije moguće pronaći odgovarajući hardver. Ime datoteke Putanja @@ -222,7 +222,7 @@ Proizvoljna boja baze podataka Verzija baze podataka Tekst - Aplikacija + Sučelje Ostalo Komprimiranje Bez @@ -239,7 +239,7 @@ Nema dovoljno memorije za učitavanje cijele baze podataka. Nije moguće učitati bazu podataka. Nije moguće učitati ključ. Pokušaje smanjiti uporabu memorije KDF. - Barem jedna akreditacija mora biti postavljena. + Barem jedan skup podataka za prijavu mora biti postavljen. Svaki niz mora imati ime polja. Nije moguće aktivirati uslugu automatskog ispunjavanja. Nije moguće premjestiti grupu u samu sebe. @@ -250,7 +250,7 @@ Nije moguće spremiti bazu podataka. Razdoblje mora biti između %1$d i %2$d sekundi. Nije moguće pronaći datoteku. Probaj je ponovo otvoriti iz upravitelja datoteka. - Nije moguće čitati akreditacije. + Nije moguće čitati podatke za prijavu. Veličina elemenata popisa Veličina teksta u popisu elemenata Mala slova @@ -266,13 +266,13 @@ Ovisno o upravitelju datoteka, KeePassDX možda neće moći pisati u tvoje spremište. Riješiti problem generiranjem novih UUID-ova za duplikate\? Korijen - Količina memorije (u bajtovima) koju će koristiti funkcija za generiranje ključeva. + Količina memorije koju će koristiti funkcija za generiranje ključeva. Ne zatvaraj aplikaciju … Zadnji pristup Posebni znakovi Podcrtaj Velika slova - Ova baza podataka još nema spremljenu akreditaciju. + Ova baza podataka još nema spremljene podatke za prijavu. Biometrija Prijavi se s KeePassDX Aktiviraj automatsko ispunjavanje za brzo ispunjavanje obrazaca u drugim aplikacijama @@ -286,7 +286,7 @@ Premijesti grupe i unose u koš za smeće prije brisanja Grupa koša za smeće Ograniči broj spremljenih povijesti po unosu - Ograniči veličinu povijesti (u bajtovima) po unosu + Ograniči veličinu povijesti po unosu Povjerenje međuspremniku Dozvoli kopiranje lozinke i zaštićenih polja u međuspremnik Upozorenje: Međuspremnik dijele sve aplikacije. Ako se kopiraju osjetljivi podaci, druga aplikacija ih može vidjeti. @@ -312,7 +312,7 @@ Vibracija tipki Zvuk tipki Dozvoli bez lozinke - Dozvoljava dodir gumba „Otvori”, ako nijedna akreditacija nije odabrana + Dozvoljava dodir gumba „Otvori”, ako nisu odabrani nikoji podaci za prijavu Izbriši lozinku Briše upisanu lozinku nakon pokušaja povezivanja s bazom podataka Zaštićeno od pisanja @@ -366,7 +366,6 @@ Istek vremena za brisanje unosa tipkovnicom Zaštiti bazu podataka od pisanja Popis blokiranja web domena - Biometrijsko otključavanje baze podataka AES Doprinos Ova oznaka već postoji. @@ -377,7 +376,7 @@ Dodatni prolazi šifriranja pružaju veću zaštitu od brutalnih napada, ali stvarno mogu usporiti učitavanje i spremanje. Traži web domene s ograničenjima poddomena Traži dijeljene informacije - Ne zaboravi aktualizirati aplikaciju najnovijim verzijama. + Redovito aktualiziraj aplikaciju instaliranjem najnovijih verzija. Za aktiviranje blokiranja, ponovo pokreni aplikaciju koja sadrži obrazac. Odaberi način razvrstavanja unosa i grupa. Pristup datoteci koju je opozvao upravitelj datoteka @@ -395,13 +394,12 @@ <strong>Doprinosom</strong>, Dodaj prilagođena polja Zaključaj bazu podataka brzo, aplikaciju možeš postaviti tako da bazu nakon nekog vremena zaključa i kad se ekran isključi. - Argon2 Twofish Prikaži mjesto nedavnih baza podataka - Za brzo otključavanje baze podataka, poveži lozinku sa skeniranom biometrijom. + Za brzo otključavanje baze podataka, poveži lozinku sa skeniranom biometrijom. Kako bismo zadržali našu slobodu i uvijek bili aktivni, računamo na tvoj<strong>doprinos.</strong> Za stvaranje ključa za algoritam šifriranja, glavni ključ se transformira pomoću funkcije za generiranje ključeva koja sadrži slučajnu komponentu. - Zaključaj bazu podataka kad korisnik pritisne gumb za natrag na ekranu + Zaključaj bazu podataka kad korisnik pritisne gumb za natrag na glavnom ekranu Sakrij pokvarene poveznice baze podataka Blokiranje automatskog ispunjavanja Baza ključeva nije ispravno inicijalizirana. @@ -409,9 +407,9 @@ Istekli unosi se ne pokazuju Zaključaj bazu podataka Otključaj bazu podataka - Automatski traži biometriju, ako je baza podataka tako postavljena + Automatski zatraži napredno otključavanje ako je baza podataka tako postavljena Nije moguće pokrenuti ovu funkciju. - Automatski otvori biometrijsku prijavu + Automatski otvori prozor za prijavu Istek vremena međuspremnika Pretraži unose Kopiraj jedno polje @@ -455,10 +453,10 @@ \nGrupe (~mape) organiziraju unose u bazi podataka. U tijeku: %1$d%% Gotovo! - Automatski se vrati na prethodnu tipkovnicu nakon izvršavanja automatske radnje tipke + Automatski se vrati na prethodnu tipkovnicu nakon izvršavanja „Automatska radnje tipke” Automatska radnja tipke - Automatski se prebaci na prethodnu tipkovnicu pri ekranu za unos podataka za prijavu u bazu podataka - Ekran za unos podataka za prijavu u bazu podataka + Automatski se prebaci na prethodnu tipkovnicu pri ekranu za unos podataka za prijavu na bazu podataka + Ekran za unos podataka za prijavu na bazu podataka Promijeni tipkovnicu Baza podataka za KeePass trebala bi sadržavati samo male datoteke uslužnih programa (poput PGP datoteke ključeva). \n @@ -475,7 +473,7 @@ Ukloni nepovezane podatke Podaci Svejedno ukloniti ove podatke\? - Podaci podataka prijave + Podaci za prijavu Pokušaj spremiti podatke pretrage prilikom odabira ručnog unosa Obavijest Nije dopušteno spremati novi element u zaštićenoj bazi podataka @@ -492,10 +490,50 @@ Pokušaj spremiti dijeljene informacije prilikom odabira ručnog unosa Spremi dijeljene informacije Trajno izbrisati sve čvorove iz smeća\? - Nije moguće dohvatiti kripto objekt. Potrebno je aktualizirati biometrijsku zaštitu. Ne postoji biometrijski ključ niti podaci za prijavu uređaja. Modus registracije Modus spremanja Modus pretrage + Ime polja već postoji. + Izbrisati sve ključeve šifriranja povezane s naprednim prepoznavanjem otključavanja\? + Upiši lozinku i zatim pritisni gumb „Napredno otključavanje”. + Otvori bazu podataka pomoću naprednog prepoznavanja otključavanja + Upozorenje: Ako koristiš prepoznavanje naprednog otključavanja morat ćeš i dalje znati glavnu lozinku. + Izbriši ključ naprednog otključavanja + Napredno prepoznavanje otključavanja + Nije moguće pokrenuti prozor naprednog otključavanja. + Greška naprednog otključavanja: %1$s + Izdvoji podatake za prijavu na bazu podataka pomoću podataka naprednog otključavanja + Otvori prozor naprednog otključavanja za spremanje podataka za prijavu + Otvori prozor naprednog otključavanja za otključavanje baze podataka + Nije moguće prepoznati digitanlni otisak za napredno otključavanje + Nije moguće pročitati ključ naprednog otključavanja. Izbriši ga i ponovi postupak prepoznavanja. + Tipka Enter + Tipka Backspace + Odaberi unos + Natrag na prethodnu tipkovnicu + Prilagođena polja + Omogućuje otvaranje baze podataka pomoću podataka za prijavu + Otključavanje s podacima za prijavu uređaja + Podaci za prijavu uređaja + Dodirni za brisanje ključeva naprednog otključavanja + Napredno otključavanje baze podataka + Vremensko ograničenje neprednog otključavanja + Trajanje korištenja naprednog otključavanja prije brisanja sadržaja + Istek naprednog otključavanja + Nemoj spremati šifrirani sadržaj za napredno otključavanje + Sadržaj + Privremeno napredno otključavanje + Argon2id + Argon2d + Nije moguće ispravno obnoviti popis. + URI baze podataka nije moguće dobiti. + Umetnuti prijedlozi + Prijedlozi za automatsko popunjavanje su dodani. + Pokušaj prikazivanja prijedloga za automatsko popunjavanje izravno s kompatibilne tipkovnice + Pristup datoteci koju je opozvao upravljač datoteka. Zatvori bazu podataka i ponovo je otvori s njezinog mjesta. + Prepiši vanjske promjene spremanjem baze podataka ili je ponovo učitaj s najnovijim promjenama. + Podaci u datoteci tvoje baze podataka izmijenjeni su izvan programa. + Ponovo učitaj bazu podataka \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 005cb1736..ef9c88588 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -182,7 +182,7 @@ Az összes adathoz használt adatbázis-titkosítási algoritmus. A kulcs előállításához a titkosítási algoritmushoz, a mesterkulcs átalakításra került egy véletlenszerűen sózott kulcselőállítási függvénnyel. Memóriahasználat - A kulcselőállítási függvényhez használt memóriamennyiség (bájtban). + A kulcselőállítási függvényhez használt memóriamennyiség. Párhuzamosság A kulcselőállítási függvény párhuzamosságának mértéke (azaz a szálak száma). Rendezés @@ -280,8 +280,6 @@ \nA csoportok (~mappák) rendszerezik a bejegyzéseket az adatbázisban. Keresés a bejegyzések között Adja meg a címet, felhasználónevet vagy más mezők tartalmát, hogy lekérje a jelszavát. - Adatbázis feloldása ujjlenyomattal - Kösse össze a jelszavát a mentett ujjlenyomatával, hogy gyorsan fel tudja oldani az adatbázist. Bejegyzés szerkesztése Erős jelszó létrehozása Állítson elő egy erős jelszót, és rendelje hozzá a bejegyzéshez, adja meg egyszerűen az űrlap feltételeinek megfelelően, és ne felejtse el biztonságosan tárolni. @@ -319,7 +317,6 @@ Támogatás ChaCha20 AES - Argon2 Alkalmazástéma Az alkalmazásban használt téma Ikoncsomag @@ -401,7 +398,7 @@ Megújítás kényszerítése A mesterkulcs módosításának javaslata (napban) Javasolt megújítás - Korlátozza az előzmények méretét bejegyzésenként (bájtban) + Korlátozza az előzmények méretét bejegyzésenként Maximális méret Korlátozza az előzmények számát bejegyzésenként Maximális szám @@ -467,7 +464,6 @@ Eltávolítja azokat a mellékleteket, melyek az adatbázisban szerepelnek, de nem tartoznak bejegyzéshez Nem összekapcsolt adatok eltávolítása Adatok - A titkosítási objektum nem kérhető le. Biometrikus biztonsági frissítés szükséges. Nincs biometrikus vagy eszközazonosító beállítva. A kulcsfájl tartalmának sosem szabad megváltoznia, és a legjobb esetben véletlenszerűen előállított adatokat kellene tartalmaznia. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 796761e9e..9f5b5db4d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -27,11 +27,11 @@ Aggiungi gruppo Algoritmo di cifratura Scadenza app - Tempo di inattività prima del blocco dell\'app + Tempo di inattività prima del blocco del database App Impostazioni app Parentesi - È necessario un file manager che accetti gli Intent ACTION_CREATE_DOCUMENT e ACTION_OPEN_DOCUMENT per creare, aprire e salvare i file di database. + Un file manager che accetta intent action ACTION_CREATE_DOCUMENT e ACTION_OPEN_DOCUMENT è necessario creare, aprire e salvare i file del database. Appunti eliminati Errore negli appunti Alcuni dispositivi non permettono alle app di usare gli appunti. @@ -39,8 +39,8 @@ Scadenza appunti Tempo prima di eliminare gli appunti (se supportato dal dispositivo) Copia %1$s negli appunti - Creazione file chiave database… - Banca dati + Recupero chiave database… + Database Decodifica contenuto database… Usa come database predefinito Numeri @@ -64,15 +64,15 @@ La codifica a flusso Arcfour non è supportata. KeePassDX non può gestire questo URI. Impossibile creare il file - Lettura del database fallita. + Impossibile leggere il database. Assicurati che il percorso sia corretto. Inserisci un nome. Memoria insufficiente per caricare l\'intero database. Deve essere selezionato almeno un tipo di generazione password. Le password non corrispondono. - \"Livello\" troppo alto. Impostato a 2147483648. + «Livello» troppo alto. Impostato a 2147483648. Ogni stringa deve avere un nome. - Inserisci un numero naturale positivo nel campo \"lunghezza\". + Inserisci un numero intero positivo nel campo \"Lunghezza\". Nome campo Valore campo File non trovato. Prova a riaprirlo dal tuo gestore di file. @@ -87,12 +87,12 @@ Password Non è possibile leggere le credenziali. Algoritmo errato. - Formato database non riconosciuto. + Formato del database non riconosciuto. Il file chiave è vuoto. Lunghezza Dimensione elenco elementi Dimensione del testo nell\'elenco del gruppo - Caricamento database… + Caricamento del database… Minuscole Nascondi le password Maschera le password (***) in modo predefinito @@ -114,34 +114,34 @@ Nessun risultato di ricerca Installa un browser web per aprire questo URL. Non cercare nelle voci di backup - Ometti i gruppi \"Backup\" e \"Cestino\" dai risultati di ricerca - Creazione nuovo database… + Ometti i gruppi «Backup» e «Cestino» dai risultati di ricerca + Creazione di un nuovo database… In corso… Protezione Sola lettura - KeePassDX richiede l\'autorizzazione di scrittura per poter modificare il tuo database. + A seconda del tuo file manager, KeepassDX potrebbe non riuscire a scrivere sulla memoria. Elimina Root Livello cifratura Livelli di cifratura aggiuntivi forniscono una maggiore protezione contro attacchi di tipo forza bruta, ma può rallentare il caricamento e il salvataggio. - Salvataggio database… + Salvataggio del database… Spazio Cerca Ordine naturale Speciali - Titolo/descrizione voce - Risultati ricerca + Cerca + Risultati della ricerca Trattino basso - Versione database non supportata. + Versione del database non supportata. Maiuscole Attenzione - Evita password con caratteri al di fuori del formato di codifica del testo nel file di database (i caratteri non riconosciuti vengono convertiti nella stessa lettera). + Evita password con caratteri al di fuori del formato di codifica del testo nel file del database (i caratteri non riconosciuti vengono convertiti nella stessa lettera). Versione %1$s Password criptata salvata - Questo database non ha ancora alcuna password. - Inserisci la password e/o il file chiave per sbloccare il database. + Questo database non contiene alcuna credenziale. + Inserisci la password o il file chiave per sbloccare la base di dati. \n -\nEseguire il backup del file di database in un luogo sicuro dopo ogni modifica. +\nEseguire il backup del file del database in un luogo sicuro dopo ogni modifica. 5 secondi 10 secondi @@ -176,7 +176,7 @@ Algoritmo di cifratura del database usato per tutti i dati. Per generare la chiave per l\'algoritmo di cifratura, la chiave principale viene trasformata usando una funzione di derivazione della chiave (con un sale casuale). Utilizzo di memoria - Quantità di memoria (in byte) utilizzabili dalla funzione di derivazione della chiave. + Quantità di memoria utilizzabili dalla funzione di derivazione della chiave. Parallelismo Grado di parallelismo (cioè numero di thread) usato dalla funzione di derivazione della chiave. Ordina @@ -188,7 +188,7 @@ Creazione Modifica Accesso - Continuare senza aver impostato una password di sblocco \? + Continuare senza aver impostato una password di sblocco\? Continuare senza una chiave di cifratura\? Cronologia Aspetto @@ -197,7 +197,7 @@ Autocompletamento di KeePassDX Accedi con KeePassDX Imposta servizio predefinito di autocompletamento - Attiva l\'autocompletamento per compilare velocemente i moduli in altre app + Attiva l\'autocompletamento per riempire velocemente i campi in altre app Dimensione password generata Imposta la dimensione predefinita delle password generate Caratteri password @@ -212,32 +212,32 @@ Scansione di impronte Consente la scansione biometrica per aprire il database Elimina chiavi di cifratura - Elimina tutte le chiavi di cifratura relative al riconoscimento dell\'impronta + Elimina tutte le chiavi di cifratura relative allo sblocco avanzato Impossibile avviare questa funzione. - La tua versione di Android %1$s non è la minima %2$s richiesta. + Il dispositivo usa Android %1$s, ma richiede %2$s o versioni successive. L\'hardware relativo non è stato trovato. Nome file Percorso Assegna una chiave master Crea un nuovo database Uso del Cestino - Sposta i gruppi e le voci nel gruppo \"Cestino\" prima di eliminarlo + Sposta i gruppi e le voci nel gruppo «Cestino» prima di eliminarlo Carattere campi Cambia il carattere usato nei campi per una migliore visibilità Fiducia appunti - Permetti la copia della password e dei campi protetti negli appunti + Consenti la copia della password e dei campi protetti negli appunti Attenzione: gli appunti sono condivisi da tutte le app. Se vengono copiati dati sensibili, altri software possono recuperarli. - Nome database - Descrizione database - Versione database + Nome del database + Descrizione del database + Versione del database Testo - App + Interfaccia Altro Tastiera Magitastiera - Attiva una tastiera personale che popola le tue password e i campi di identità + Attiva una tastiera personale che inserisce le tue password e i campi di identità Non consentire nessuna chiave principale - Abilita il pulsante «Apri» se le credenziali non sono selezionate + Permetti di toccare il pulsante \"Apri\" se non sono selezionate credenziali Protetto da scrittura Apri il database in sola lettura in modo predefinito Suggerimenti educativi @@ -245,67 +245,63 @@ Ripristina i suggerimenti educativi Mostra di nuovo tutte le informazioni educative Suggerimenti educativi ripristinati - Crea il tuo file di database + Crea il tuo file database Crea il tuo primo file di gestione password. - Apri un database esistente - Apri il tuo file database precedente dal tuo gestore di file per continuare ad usarlo. + Apri un database esitente + Apri il tuo file database usato in precedenza con il file manager per continuare ad usarlo. Aggiungi elementi al tuo database - Gli elementi aiutano a gestire le tue identità digitali. -\n -\nI gruppi (~ cartelle) organizzano gli elementi nel database. + Gli elementi aiutano a gestire le tue identità digitali. +\n +\nI gruppi (come cartelle) organizzano gli elementi nel database. Cerca tra gli elementi Inserisci il titolo, il nome utente o il contenuto di altri campi per recuperare le tue password. - Sblocco del database tramite impronta digitale - Collega la password alla tua impronta digitale per sbloccare velocemente il database. Modifica l\'elemento Modifica l\'elemento con campi personalizzati. I dati possono fare riferimento ad altri campi. Crea una password robusta Genera una password robusta da associare all\'elemento, definiscila a seconda dei criteri del modulo e non dimenticare di tenerla al sicuro. Aggiungi campi personalizzati - Registra un campo aggiuntivo, inserisci delle informazioni e proteggilo se necessario. + Registra un campo aggiuntivo, aggiungi un valore e facoltativamente proteggilo. Sblocca il tuo database Proteggi da scrittura il tuo database - Cambia la modalità di apertura per la sessione. -\n -\n\"Sola lettura\" impedisce modifiche accidentali al database. -\n\"Modificabile\" permette di aggiungere, eliminare o modificare tutti gli elementi. + Cambia la modalità di apertura per la sessione. +\n +\n«Sola lettura» impedisce modifiche accidentali al databae. +\n«Modificabile» permette di aggiungere, eliminare o modificare tutti gli elementi. Copia un campo I campi copiati possono essere incollati ovunque. \n \nUsa il metodo di inserimento che preferisci. Blocca il database - Blocca velocemente il database, puoi impostare l\'app per bloccarlo dopo un certo periodo e quando lo schermo si spegne. + Blocca velocemente il database, puoi impostare l\'applicazione per bloccarsi dopo un certo periodo e quando lo schermo si spegne. Ordine elementi Scegli l\'ordine di elementi e gruppi. Partecipa Aiuta a migliorare la stabilità, la sicurezza e ad aggiungere nuove funzioni. Diversamente da molte app di gestione password, questa è <strong>senza pubblicità</strong>, <strong>software libero (copyleft)</strong> e non raccoglie dati personali nei suoi server, non importa quale versione usi. - Acquistando la versione pro, avrai accesso a questo <strong>tema</strong> e soprattutto aiuterai nella <strong>realizzazione di progetti della comunità.</strong> - Questo<strong>tema</strong> è disponibile grazie alla tua generosità. - Per mantenere la nostra libertà ed essere sempre attivi, contiamo sul tuo <strong>contributo.</strong> - + Acquistando la versione pro, avrai accesso a questa <strong>stile visivo</strong> e soprattutto aiuterai nella <strong>realizzazione dei progetti della comunità.</strong> + Questa <strong>stile visivo</strong> è disponibile grazie alla tua generosità. + Al fine di mantenere la nostra libertà ed essere sempre attivi, contiamo sul tuo <strong>contributo.</strong> Questa funzione è <strong>in sviluppo</strong> e richiede il tuo <strong>contributo</strong> per essere disponibile a breve. Acquistando la versione <strong>pro</strong>, <strong>Contribuendo</strong>, - incoraggi gli sviluppatori a creare <strong>nuove funzioni</strong> e a <strong>correggere errori</strong> secondo le tue osservazioni. + stai incoraggiando gli sviluppatori a creare <strong>nuove funzionalità</strong> e a <strong>correggere errori</strong> in base alle tue osservazioni. Grazie mille per il tuo contributo. Stiamo lavorando sodo per rilasciare questa funzione a breve. - Non dimenticare di tenere aggiornata l\'app installando nuove versioni. + Ricorda di tenere aggiornata l\'app installando le nuove versioni. Scarica Contribuisci Rijndael (AES) Twofish ChaCha20 AES - Argon2 Tema dell\'app Tema usato nell\'app Pacchetto icone Pacchetto icone usato nell\'app Modifica elemento Caricamento del database fallito. - Caricamento della chiave fallito. Prova a diminuire l\' \"Utilizzo memoria\" del KDF. + Caricamento della chiave fallito. Prova a diminuire l\'«Utilizzo memoria» del KDF. Mostra nomi utente Mostra i nomi utente negli elenchi Appunti @@ -331,7 +327,7 @@ Modalità selezione Non terminare l\'app… Premere \'\'Indietro\'\' per bloccare - Bloccare il database quando l\'utente preme il pulsante Indietro nella schermata principale + Blocca il database quando l\'utente preme il pulsante Indietro nella schermata principale Pulisci alla chiusura Blocca il database quando scade la durata degli appunti o la notifica viene chiusa dopo che inizi ad usarlo Cestino @@ -372,8 +368,8 @@ Contatore Cifre Algoritmo - Password usa e getta - Segreto per password usa e getta (OTP) non valido. + Password usa e getta (OTP) + Segreto della password usa e getta (OTP) non valido. Impostare almeno una credenziale. Non puoi copiare un gruppo qui. La chiave segreta deve essere nel formato Base32. @@ -383,9 +379,9 @@ %1$s con le stesse credenziali univoche %2$s è già esistente. Sto creando il database… Impostazioni di sicurezza - Il databse contiene identificativi univoci univerali duplicati. + Il database contiene Identificativi Univoci Universali (UUID) duplicati. Non è possibile salvare il database. - Salva il database + Salva database Svuota il cestino Esecuzione del comando… Vuoi eliminare definitivamente i nodi selezionati\? @@ -399,16 +395,16 @@ Il keystore non è inizializzato correttamente. Impostazioni della chiave principale Chiave principale - Contribuisci - Garantisci il permesso di scrittura per salvare i cambiamenti del database - Nascondi link corrotti nella lista dei database recenti - Nascondi i link di database corrotti + Contributi + Concedi il permesso di scrittura per salvare i cambiamenti del database + Nascondi collegamenti corrotti nella lista dei database recenti + Nascondi i collegamenti dei database corrotti Mostra le posizioni dei database recenti Mostra file recenti - Salva la posizione dei keyfile + Ricorda posizione file chiave Ricorda la posizione dei database - Salva posizione dei database - Per continuare, risolvi il problema generando nuovi UUIDs per i duplicati \? + Ricorda posizione database + Risolvi il problema generando nuovi UUID per i duplicati per continuare\? Impossibile creare il file del database. Aggiungi allegato Scarta @@ -416,51 +412,51 @@ Convalida Dimensione massima Numero massimo - Apri automaticamente prompt biometrico - Limita la dimensione (in byte) della cronologia per voce + Apri automaticamente la richiesta + Limita la dimensione della cronologia per voce Limita il numero di elementi della cronologia per voce Gruppo cestino La compressione dei dati riduce le dimensioni del database Compressione dati - Proponi l\'autenticazione biometrica quando il database è configurato per usarla + Richiedi automaticamente lo sblocco avanzato se il database è impostato per usarlo Utilizza lo sblocco avanzato per aprire il database più facilmente Copia i campi di immissione utilizzando gli appunti del tuo dispositivo Database aperto Biometrico Forza rinnovo - Consigliato cambiare la chiave principale (giorni) + È consigliato di cambiare la chiave principale (giorni) Rinnovo raccomandato - I record scaduti sono nascosti - Nascondi i record scaduti - Imposta la gestione delle OTP (HOTP / TOTP) per generare un token richiesto per la 2FA. - Completo! + Le voci scadute non sono mostrate + Nascondi le voci scadute + Imposta la gestione della password usa e getta (HOTP/TOTP) per generare un token richiesto per l\'autenticazione a due fattori. + Completato! Finalizzazione… Avanzamento %1$d%% Inizializzazione… Scaricamento %1$s - Imposta OTP - Salva il database dopo ogni azione importante (in modalità \"Modificabile\") + Imposta One-Time Password (OTP) + Salva il database dopo ogni azione importante (in modalità «Modificabile») Salvataggio automatico del database Suggerisci automaticamente risultati dal dominio web o ID dell\'applicazione Ricerca automatica - Dopo la pressione del tasto \"Campo\" invia il tasto \"Vai\" - Azione auto key - Impostazioni tastiera dispositivo + Azione del tasto \"Vai\" dopo aver premuto un tasto \"Campo\" + Tasto di azione automatica + Impostazioni della tastiera del dispositivo Gzip Nessuna Compressione - Colore del database customizzato - Nome utente di default + Colore personalizzato del database + Nome utente predefinito Disabilita Abilita - Richiedi il cambio della master key la prossima volta (una volta) + Richiedi il cambio della chiave principale la prossima volta (una volta) Forza il rinnovo la prossima volta - Richiedi il cambio della master key (giorni) + Richiedi la modifica della chiave principale (giorni) Mostra il bottone di blocco nell\'interfaccia utente Mostra il bottone di blocco Impostazioni dell\'autocompletamento Accesso al file revocato dal file manager - Ricorda la posizione dei keyfiles dei database + Ricorda la posizione dei file chiave Questa etichetta esiste già. Riavvia l\'app contenente il campo per attivare il blocco. Blocca riempimento automatico @@ -477,8 +473,8 @@ Aggiungi elemento Torna automaticamente alla tastiera precedente quando si esegue l\'azione del tasto automatico Azione tasto automatico - Torna automaticamente alla tastiera precedente nella schermata delle credenziali del database - Schermata credenziali database + Torna automaticamente alla tastiera precedente nella schermata credenziali del database + Schermata credenziali del database Cambia tastiera Carica %1$s Carica un allegato alla voce per salvare dati esterni importanti. @@ -494,6 +490,69 @@ Caricare questo file sostituirà quello esistente. Un database KeePass dovrebbe contenere solo piccoli file di utilità (come i file di chiavi PGP). \n -\nIl database può diventare molto grande e ridurre le prestazioni con questo caricamento. +\nIl tuo database può diventare molto grande e ridurre le prestazioni con questo caricamento. Info credenziali + Visualizza l\'UUID collegato a una voce + Mostra UUID + Il salvataggio dei dati non è consentito per una base du dati aperta in sola lettura. + Chiedi di salvare i dati quando un modulo viene convalidato + Chiedi di salvare i dati + Prova a salvare le informazioni di ricerca quando effettui una selezione di immissione manuale + Salva le informazioni di ricerca + Chiudi il database dopo aver usato l\'autocompletamento + Chiudi database + Torna automaticamente alla tastiera precedente dopo aver bloccato il database + Blocca il database + Prova a salvare le informazioni condivise quando effettui una selezione di immissione manuale + Salva le informazioni condivise + Notifica + È necessario un aggiornamento della sicurezza biometrica. + Eliminare definitivamente tutti i nodi dal cestino\? + Modalità registrazione + Modalità salvataggio + Modalità ricerca + Il nome del campo esiste già. + Il salvataggio di un nuovo elemento non è consentito in un database di sola lettura + Nessuna credenziale biometrica o del dispositivo è registrata. + Collega la password alla tua autenticazione biometrica (o del dispositivo) per sbloccare velocemente il database. + Sblocco avanzato del database + Invio + Backspace + Seleziona voce + Torna alla tasitera precedente + Campi personalizzati + Vuoi eliminare le chiavi di cifratura relative allo sblocco avanzato\? + Scadenza sblocco avanzato + Non salvare alcun contenuto criptato per usare lo sblocco avanzato + Validità dello sblocco avanzato prima di eliminarne il contenuto + Scadenza sblocco avanzato + Sblocco avanzato temporaneo + Utilizza le credenziali del dispositivo per sbloccare il database + Sblocco con credenziali dispositivo + Tocca per eliminare le chiavi di sblocco avanzato + Contenuto + Non è possibile inizializzare lo sblocco avanzato. + Non è possibile riconoscere lo sblocco avanzato + Non è possibile leggere la chiave di sblocco avanzato. Eliminala e ripeti la prodecura dello sblocco. + Estrai le credenziali del database con i dati dallo sblocco avanzato + Attenzione: dovrai sempre ricordare la password principale anche se usi lo sblocco avanzato. + Riconoscimento con sblocco avanzato + Credenziali del dispositivo + Inserisci la password, quindi clicca sull\'icona \"Sblocco avanzato\". + Errore sblocco avanzato: %1$s + Apri il database con lo sblocco avanzato + Autentica con lo sblocco avanzato per sbloccare il database + Autentica con lo sblocco avanzato per salvare le credenziali + Elimina chiave di sblocco avanzato + Argon2id + Argon2d + Non è possibile ricostruire la lista correttamente. + Non è stato recuperato l\'indirizzo del database. + Suggerimento di riempimento aggiunto. + Mostra i suggerimenti di riempimento campi in una tastiera compatibile + Suggerimenti in linea + L\'accesso al file è stato revocato dal file manager, chiudi il database e riaprilo dalla sua posizione originale. + Sovrascrivi le modifiche esterne salvano il database o ricaricalo con gli ultimi cambiamenti. + I dati nel tuo database sono stati modificati al di fuori di questa app. + Ricarica database \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index f2cfc3a6d..c10c9157e 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . translated by Arthur Zamarin ---> - +--> משוב דף הבית KeePassDX היא תוכנה המממשת את מנהל הסיסמאות KeePass לאנרואיד. @@ -27,7 +26,7 @@ הוסף קבוצה אלגוריתם פסק זמן ליישום - זמן לפני נעילת מסד הנתונים כאשר היישום לא פעיל. + זמן לפני נעילת מסד הנתונים כאשר היישום לא פעיל יישום הגדרות יישום סוגריים diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 534ccab53..035412598 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -226,7 +226,7 @@ 変換ラウンド 変換ラウンドを増やすことでブルート フォース攻撃に対する保護が強化されますが、読み込みと保存が本当に遅くなる可能性があります。 メモリ使用量 - 鍵導出関数が使用するメモリの量(バイト単位)です。 + 鍵導出関数が使用するメモリの量です。 並列処理 鍵導出関数が使用する並列処理のレベル(スレッド数)です。 データベースを保存しています… @@ -273,7 +273,6 @@ キーストアが正しく初期化されていません。 保存された暗号化済みパスワード データベースの保存済み認証情報はありません。 - crypto オブジェクトを取得できません。 履歴 デザイン 生体認証 @@ -305,10 +304,10 @@ 高度なロック解除を使用して、データベースをより簡単に開きます 生体認証によるロック解除 生体情報をスキャンしてデータベースを開くことができるようにします - 生体認証プロンプトを自動で開く - データベースが生体認証を使用するように設定されている場合、生体情報の取得を自動的に求めます + プロンプトを自動で開く + データベースに設定されている場合、高度なロック解除を自動的に要求します 暗号鍵を削除 - 生体認証に関するすべての暗号鍵を削除します + 高度なロック解除に関するすべての暗号鍵を削除します この機能を起動できませんでした。 デバイスは Android %1$s を実行していますが、%2$s 以降が必要です。 対応するハードウェアが見つかりませんでした。 @@ -326,14 +325,14 @@ 最大値 エントリーあたりの履歴項目の数を制限します 最大サイズ - エントリーあたりの履歴のサイズ(バイト単位)を制限します + エントリーあたりの履歴のサイズを制限します 更新を推奨 マスターキーの変更を推奨します(日数) 更新を強制 マスターキーの変更を必須にします(日数) 次回更新を強制 次回マスターキーの変更を必須にします(1 回のみ) - フィールド フォント + フィールド書体 フィールド内で使用するフォントを変更して、文字を見やすくします クリップボードの信頼 エントリーのパスワードと保護されたフィールドを、クリップボードにコピーすることを許可します @@ -349,7 +348,7 @@ カスタムカラー データベースのバージョン テキスト - アプリ + インターフェース その他 圧縮 なし @@ -389,7 +388,7 @@ データベース認証情報の画面 データベース認証情報の画面で、切り替え前のキーボードへ自動的に戻します 自動キーアクション - 自動キーアクションの実行後、切り替え前のキーボードへ自動的に戻します + [自動キーアクション] の実行後、切り替え前のキーボードへ自動的に戻します データベースをロック データベースのロック後、切り替え前のキーボードへ自動的に戻します データベースを閉じる @@ -427,8 +426,6 @@ エントリーはデジタル ID の管理に役立ちます。\n\nグループ(≒フォルダ)はデータベース内のエントリーを整理します。 エントリーを検索 パスワードを取得するには、タイトル、ユーザー名、または他のフィールドの内容を入力します。 - 生体認証によるロック解除 - スキャンした生体情報にパスワードをリンクして、データベースのロックをすばやく解除します。 エントリーを編集 エントリーをカスタム フィールドとともに編集します。共有データは異なるエントリーのフィールド間で参照することができます。 強力なパスワードを作成 @@ -442,7 +439,10 @@ データベースのロックを解除 データベースのロックを解除するには、パスワードまたはキーファイル、またはその両方を入力します。\n\nデータベース ファイルは変更するたびに安全な場所へバックアップしてください。 データベースの書き込みを禁止 - セッションのロック解除モードを変更します。\n\n[書き込み禁止] では、データベースに対する意図しない変更を防ぐことができます。\n[変更可能] では、すべての要素を追加、削除、変更できます。 + セッションのロック解除モードを変更します。 +\n +\n[書き込み禁止] では、データベースに対する意図しない変更を防ぐことができます。 +\n[変更可能] では、すべての要素を必要に応じて追加、削除、変更できます。 フィールドをコピー コピーしたフィールドはどこにでも貼り付けることができます。\n\nお好みのフォーム入力方法を使用してください。 データベースをロック @@ -474,7 +474,6 @@ Twofish ChaCha20 AES - Argon2 5秒 10秒 @@ -511,4 +510,46 @@ データの保存は読み取り専用として開かれたデータベースでは許可されていません。 保存モード 検索モード + フィールド名はすでに存在します。 + 高度なロック解除用の鍵を削除 + 高度なロック解除を使って認証できませんでした + エンター + バックスペース + 前のキーボードに戻る + カスタム フィールド + 高度なロック解除に関する暗号鍵をすべて削除しますか? + デバイス認証情報を使用してデータベースを開くことができるようにします + デバイス認証情報によるロック解除 + デバイス認証情報 + パスワードを入力し、[高度なロック解除] ボタンをタップします。 + 高度なロック解除プロンプトを初期化できません。 + 高度なロック解除のエラー:%1$s + 高度なロック解除用の鍵が読み取れません。削除してロック解除の手順をやり直してください。 + 高度なロック解除を使ってデータベース認証情報を取り出します + 高度なロック解除を使ってデータベースを開く + 警告:高度なロック解除を使用する場合も、マスター パスワードを記憶する必要があります。 + 高度なロック解除の認証 + ログイン プロンプトを開いて認証情報を保存する + ログイン プロンプトを開いてロックを解除する + エントリーを選択 + スキャンした生体情報またはデバイス認証情報にパスワードをリンクして、データベースのロックをすばやく解除します。 + データベースの高度なロック解除 + 高度なロック解除のタイムアウト + コンテンツを削除して高度なロック解除を終了するまでの期間 + 高度なロック解除の有効期限 + 高度なロック解除に使用する暗号化コンテンツを保存しません + 一時的な高度なロック解除 + タップして高度なロック解除用の鍵を削除する + コンテンツ + Argon2id + Argon2d + データベースの URI が取得できません。 + 自動入力候補が追加されました。 + 自動入力候補を互換性のあるキーボード上に直接表示することを試みます + インライン自動入力 + ファイルへのアクセス権がファイル マネージャーによって取り消されました。データベースを閉じて、ファイルの場所から再度開いてください。 + データベースを保存して外部の変更を上書きするか、再度読み込んで最新の変更を反映させてください。 + データベース ファイルに含まれる情報は、アプリの外部で変更されています。 + データベースを再度読み込む + リストを正しく再構築できません。 \ No newline at end of file diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index b36e9c87b..b8013d4f2 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -94,7 +94,7 @@ Rakto failas Įrašo pavadinimas/aprašymas Pakeisti master raktą - Naudota: + Naudota Maskuoti slaptažodį Tarpas Specialus diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 8736cc9c4..05c33cb72 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -25,7 +25,7 @@ Jauna grupa Šifrēšanas algoritms Pielikuma taimauts - Bloķēšanas taimauts, kad programma nav aktīva. + Bloķēšanas taimauts, kad programma nav aktīva Programma Programmas iestatījumi Iekavas diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index ba7ea9c8d..848d93870 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -192,7 +192,6 @@ വിപുലീകരിച്ച ASCII ആപ്പിൽ ഉപയോഗിച്ചിരിക്കുന്ന ഐക്കൺ പാക്ക് അപ്പ്ലിക്കേഷന്റെ തീം - Argon2 അന്തിമമാക്കുന്നു. . . നിങ്ങളുടെ സംഭാവനയ്ക്ക് ഒരുപാട് നന്ദി. ഡാറ്റാബേസ് തുറക്കുക @@ -301,7 +300,7 @@ മുമ്പുള്ള ഗ്രൂപ്പുകൾ ആദ്യത്തേത് ഏറ്റവും കുറഞ്ഞത്↓ കീ ഡെറിവേഷൻ ഫംഗ്ഷൻ ഉപയോഗിക്കുന്ന സമാന്തരതയുടെ ബിരുദം (അതായത് ത്രെഡുകളുടെ എണ്ണം). - കീ ഡെറിവേഷൻ ഫംഗ്ഷൻ ഉപയോഗിക്കുന്ന മെമ്മറിയുടെ അളവ് (ബൈറ്റുകളിൽ). + കീ ഡെറിവേഷൻ ഫംഗ്ഷൻ ഉപയോഗിക്കുന്ന മെമ്മറിയുടെ അളവ് . എല്ലാ ഡാറ്റയ്ക്കും ഉപയോഗിക്കുന്ന ഡാറ്റാബേസ് എൻ‌ക്രിപ്ഷൻ അൽ‌ഗോരിതം. സമീപകാല ഡാറ്റാബേസുകളുടെ പട്ടികയിൽ‌ നിന്ന് തകർന്ന ലിങ്കുകൾ‌ മറയ്‌ക്കുക തകർന്ന ഡാറ്റാബേസ് ലിങ്കുകൾ മറയ്‌ക്കുക @@ -369,7 +368,6 @@ അറിയിപ്പ് വിവരം എൻ‌ട്രികളും ഗ്രൂപ്പുകളും എങ്ങനെ അടുക്കുന്നുവെന്ന് തിരഞ്ഞെടുക്കുക. ഇനം തരംതിരിക്കൽ - ബയോമെട്രിക് ഡാറ്റാബേസ് അൺലോക്കുചെയ്യൽ ടൈം ഔട്ട് ഫീൽഡിന്റെ ഫോണ്ട് ഡാറ്റ @@ -390,4 +388,12 @@ രജിസ്ട്രേഷൻ മോഡ് തിരയൽ മോഡ് read-only ഡാറ്റാബേസിൽ പുതിയ ഒരു ഇനം സംരക്ഷിക്കാൻ കഴിയില്ല + വിപുലമായ ഡാറ്റാബേസ് അൺലോക്ക് + എൻ‌ട്രി തിരഞ്ഞെടുക്കുക + ഇഷ്‌ടാനുസൃത ഫീൽഡുകൾ + കീകൾ + അറിയിപ്പ് + പുതുക്കൽ ശുപാർശ ചെയ്യുക + ഉള്ളടക്കം + ഉപകരണ ക്രെഡൻഷ്യൽ \ No newline at end of file diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 18f562846..d9a4a2507 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -145,7 +145,7 @@ Krypteringsomganger Flere krypteringsomganger gir ytterligere beskyttelse mot råmaktsangrep, men kan virkelig sakke ned innlasting og lagring. Minnebruk - Mengden minne (i byte) brukt til nøkkelutledelsesfunksjonen. + Mengden minne brukt til nøkkelutledelsesfunksjonen. Parallellitet Graden av parallellitet (dvs. antallet tråder) brukt av nøkkelutledingsfunksjonen. Lagrer database… @@ -240,8 +240,6 @@ \nLegg til grupper (tilsvarende mapper) for å organisere dine oppføringer og databasen din. Søk i dine oppføringer Søk etter oppføringer etter tittel, brukernavn eller andre felter for å hente passordene dine. - Lås opp databasen din med fingeravtrykket ditt - Lenk passordet og fingeravtrykket ditt for å låse opp databasen din enkelt. Rediger oppføringen Rediger din oppføring med egendefinerte felter, referanser til pooldata kan legges til mellom felter av forskjellige oppføringer. Opprett et sterkt passord @@ -286,7 +284,6 @@ Twofish ChaCha20 AES - Argon2 Velg en drakt Tilpass programdrakten ved å endre fargene Velg en ikonpakke @@ -419,4 +416,13 @@ Innvilg skrivetilgang for å lagre databaseendringer Skjul ødelagte lenker i listen over nylige databaser Skjul ødelagte databaselenker + Spør om lagring av data + Avansert opplåsningsfeil: %1$s + Det anbefales ikke å legge til en tom nøkkelfil. + Legg til filen uansett\? + Registreringsmodus + Lagringsmodus + Søkemodus + Feltnavnet finnes allerede. + Legg til element \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c4540487a..05ae02d30 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -34,7 +34,7 @@ Bestandsbeheer dat de Intent-actie ACTION_CREATE_DOCUMENT en ACTION_OPEN_DOCUMENT accepteert, is nodig om databasebestanden aan te maken, te openen en op te slaan. Klembord gewist Klembordtime-out - Tijd van opslag op het klembord (indien ondersteund op jouw apparaat) + Tijd van opslag op het klembord (voor zover ondersteund op dit apparaat) Selecteer om %1$s naar klembord te kopiëren Databasesleutel ophalen… Database @@ -67,7 +67,7 @@ Onvoldoende vrij geheugen om de gehele database te laden. Je moet minimaal één soort wachtwoordgenerering kiezen. De wachtwoorden komen niet overeen. - \"Cycli-waarde\" te groot. Wordt ingesteld op 2147483648. + \"Cycli-waarde\" te groot. Deze wordt ingesteld op 2147483648. Voer een positief geheel getal in in het veld \"Lengte\". Bestandsbeheer Wachtwoord genereren @@ -81,16 +81,16 @@ Kan referenties niet lezen. Databaseformaat kan niet worden herkend. Lengte - Lengte van lijst met items - Tekstgrootte in de lijst + Lijstgrootte + Tekstgrootte in de itemslijst Database laden… Kleine letters Wachtwoorden verbergen - Wachtwoorden standaard verbergen (***) + Wachtwoorden standaard maskeren (***) Over Hoofdsleutel wijzigen Instellingen - Instellingen database + Database-instellingen Verwijderen Doneren Bewerken @@ -180,7 +180,7 @@ Database-encryptie-algoritme dat voor alle gegevens wordt gebruikt. Om de sleutel voor het algoritme te kunnen genereren, wordt de hoofdsleutel getransformeerd middels een willekeurige afleidingsfunctie. Geheugengebruik - De hoeveelheid geheugen (bytes) dat de afleidingsfunctie mag gebruiken. + De hoeveelheid geheugen dat de afleidingsfunctie mag gebruiken. Parallellen Het aantal parallellen (aantal threads) dat de afleidingsfunctie mag gebruiken. Sorteren @@ -205,15 +205,15 @@ Auto-aanvullen KeePassDX auto-aanvullendienst Inloggen met KeePassDX - Dienst voor automatisch aanvullen + Dienst automatisch aanvullen Schakel de dienst in om formulieren in andere apps in te vullen Gegenereerde wachtwoordlengte - Standaardlengte van gegenereerd wachtwoord instellen + Stel de standaardlengte van gegenereerd wachtwoord in Wachtwoordtekens Toegestane wachtwoordtekens instellen Klembord Klembordmeldingen - Toon klembordmeldingen om velden te kopiëren bij het bekijken van een item + Schakel klembordmeldingen in om velden te kopiëren bij het bekijken van een item Als automatisch wissen van het klembord mislukt, doe dit dan handmatig. Vergrendelen Schermvergrendeling @@ -222,9 +222,9 @@ Ontgrendelen met biometrie Gebruik biometrische herkenning om de database te openen Coderingssleutels verwijderen - Alle sleutels voor biometrische herkenning verwijderen + Alle coderingssleutels met betrekking tot geavanceerde ontgrendelingsherkenning verwijderen Kan deze functie niet starten. - Het apparaat draait op Android %1$s, maar %2$s of hoger is vereist. + Dit apparaat draait op Android %1$s, maar %2$s of hoger is vereist. De bijbehorende hardware werd niet gevonden. Bestandsnaam Pad @@ -241,19 +241,19 @@ Databaseomschrijving Databaseversie Tekst - App + Gebruikersomgeving Overig Toetsenbord Magikeyboard - Aangepast toetsenbord met je wachtwoorden en alle identiteitsvelden activeren + Activeer een aangepast toetsenbord dat je wachtwoorden en identiteitsvelden vult Geen hoofdwachtwoord toestaan - Maakt het mogelijk op de knop \"Openen\" te tikken als er geen inloggegevens zijn geselecteerd + Schakel de knop \"Openen\" in als er geen referenties zijn geselecteerd Alleen-lezen Open de database standaard alleen-lezen Informatieve tips Markeer elementen om te leren hoe de app werkt Informatieve tips opnieuw instellen - Informatieve tips opnieuw weergeven + Informatieve tips opnieuw tonen Informatieve tips opnieuw ingesteld Maak je databasebestand aan Maak je eerste wachtwoordbeheerbestand aan. @@ -265,8 +265,6 @@ \nGroepen (~mappen) organiseren de items in je database. Doorzoek al je items Doorzoek items op titel, gebruikersnaam of andere velden om wachtwoorden te vinden. - Biometrische database -ontgrendeling - Koppel je wachtwoord aan je biometrie om de database snel te ontgrendelen. Item bewerken Bewerk het item met aangepaste velden. Referenties kunnen worden toegevoegd tussen velden van verschillende items. Genereer een sterk wachtwoord @@ -275,10 +273,10 @@ Registreer een extra veld, voeg een waarde toe en bescherm dit desgewenst. Ontgrendel je database Database alleen-lezen - Wijzig de opstartmodus van de sessie. -\n -\nIn alleen-lezenmodus kunnen geen onbedoelde wijzigingen worden gemaakt. -\nIn schrijfmodus kun je elementen toevoegen, verwijderen of aanpassen. + Wijzig de opstartmodus van de sessie. +\n +\nIn alleen-lezenmodus kunnen geen onbedoelde wijzigingen worden gemaakt. +\nIn schrijfmodus kun je naar wens elementen toevoegen, verwijderen of aanpassen. Veld kopiëren toestaan Kopieer een veld en plak de inhoud waar je maar wilt. \n @@ -299,16 +297,15 @@ motiveer je ontwikkelaars om <strong>nieuwe functies</strong> te creëren en <strong>problemen op te lossen</strong>. Hartelijk bedankt voor je bijdrage. We zijn druk bezig om deze functie snel beschikbaar te stellen. - Vergeet niet om je app up-to-date te houden door nieuwe versies te installeren. + Vergeet niet je app up-to-date te houden door nieuwe versies te installeren. Downloaden Bijdragen ChaCha20 AES - Argon2 App-thema Thema gebruikt in de app - Pictogrammenverzameling - Pictogrammenverzameling in gebruik + Icon pack + Gebruikt Icon Pack Build %1$s Magikeyboard Magikeyboard (KeePassDX) @@ -331,14 +328,14 @@ Selectiemodus App niet afsluiten… Druk \'Terug\' om te vergrendelen - Vergrendel de database wanneer de gebruiker op de knop Terug in het hoofdscherm klikt + Vergrendel de database wanneer de gebruiker in het hoofdscherm op de knop Terug klikt Wissen bij afsluiten Vergrendel de database wanneer de duur van het klembord verloopt of de melding wordt gesloten nadat u deze bent gaan gebruiken Prullenmand Itemselectie Invoervelden in Magikeyboard tonen bij het bekijken van een item Wachtwoord wissen - Wis het ingevoerde wachtwoord na een poging met een database te verbinden + Wis het ingevoerde wachtwoord na een verbindingspoging met een database Bestand openen Onderliggende items Knooppunt toevoegen @@ -356,7 +353,7 @@ UUID Je kan hier geen item plaatsen. Je kan hier geen item kopiëren. - Toon het aantal items + Aantal items tonen Toon het aantal items in een groep Achtergrond Update @@ -364,8 +361,8 @@ Kan geen database aanmaken met dit wachtwoord en sleutelbestand. Geavanceerd ontgrendelen Biometrie - Automatisch om biometrie vragen - Automatisch om biometrie vragen als een database hiervoor is ingesteld + Auto-open suggestie + Automatisch om geavanceerde ontgrendeling vragen als een database hiervoor is ingesteld Inschakelen Uitschakelen Hoofdsleutel @@ -393,14 +390,14 @@ De database bevat dubbele UUID\'s. Probleem oplossen door nieuwe UUID\'s te genereren voor de duplicaten\? Database geopend - Kopieer invoervelden met behulp van het klembord van uw apparaat + Kopieer invoervelden met behulp van het klembord van dit apparaat Geavanceerde ontgrendeling gebruiken om een database gemakkelijker te openen Gegevenscompressie Gegevenscompressie verkleint de omvang van de database Maximum aantal Beperk het aantal geschiedenisitems per item Maximum - Geschiedenis (bytes) per item beperken + Geschiedenis per item beperken Vernieuwing aanbevelen Aanbeveling de hoofdsleutel te wijzigen (dagen) Vernieuwing afdwingen @@ -416,20 +413,20 @@ Sla de database op na elke belangrijke actie (in \"Schrijf\" modus) OTP instellen Locatie van sleutelbestanden opslaan - Databaselocaties onthouden + Databaselocaties opslaan Verlopen items worden niet getoond - Verberg verlopen items - Klaar! + Verlopen items verbergen + Voltooid! Voltooien… Voortgang: %1$d%% Initialiseren… Download %1$s - Stel eenmalig wachtwoordbeheer (HOTP / TOTP) in om een token te genereren dat wordt gevraagd voor tweefactorauthenticatie (2FA). + Stel eenmalig wachtwoordbeheer (HOTP / TOTP) in om een token te genereren voor tweefactorauthenticatie (2FA). Automatisch opslaan Automatisch zoekresultaten voorstellen vanuit het webdomein of de toepassings-ID Automatisch zoeken Prullenbak - Geeft de vergrendelknop weer in de gebruikersinterface + Geef de vergrendelknop weer in de gebruikersinterface Vergrendelknop weergeven Instellingen voor automatisch aanvullen De sleutelopslag is niet correct geïnitialiseerd. @@ -437,10 +434,10 @@ Toegang tot het bestand ingetrokken door bestandsbeheer Bestandstoegang verlenen om databasewijzigingen op te slaan Opdracht uitvoeren… - Verberg gebroken links in de lijst met recente databases - Verberg corrupte databasekoppelingen - Onthoudt waar de databasesleutelbestanden zijn opgeslagen - Onthoudt waar de databases zijn opgeslagen + Gebroken links in de lijst met recente databases verbergen + Corrupte databasekoppelingen verbergen + Onthoud de locatie van databasesleutelbestanden + Onthoud de locatie van databases Zoekopdracht aanmaken bij het openen van een database Snel zoeken Geschiedenis wissen @@ -473,19 +470,19 @@ Item toevoegen \"Gaan\"-toetsactie na het indrukken van een \"Veld\"-toets Automatische toetsactie - Schakel automatisch terug naar het vorige toetsenbord na het uitvoeren van de Automatische toetsactie + Schakel automatisch terug naar het vorige toetsenbord na het uitvoeren van de \"Automatische toetsactie\" Automatische toetsactie Schakel automatisch terug naar het vorige toetsenbord op het databasereferentiescherm Scherm Databasereferenties Van toetsenbord wisselen - %1$s uploaden - Upload een bijlage bij dit item om belangrijke externe gegevens op te slaan. + Upload %1$s + Voeg een bijlage toe aan dit item om belangrijke externe gegevens op te slaan. Bijlage toevoegen - Toch het bestand toevoegen\? - Een KeePass-database mag alleen kleine hulpprogramma-bestanden bevatten (zoals PGP-sleutelbestanden). + Het bestand toch toevoegen\? + Een KeePass database is bedoeld om alleen kleine gebruiksbestanden te bevatten (zoals PGP sleutelbestanden). \n -\nDe database kan erg groot worden en de prestaties kunnen verminderen bij deze upload. - Als je dit bestand uploadt, wordt het bestaande vervangen. +\nMet deze upload kan de database erg groot worden en kunnen de prestaties verminderen. + Uploaden van dit bestand zal het bestaande bestand vervangen. Inloggegevens Het verwijderen van niet-gekoppelde gegevens kan de omvang van uw database verkleinen, maar kan ook gegevens verwijderen die voor KeePass-plug-ins worden gebruikt. Deze gegevens toch verwijderen\? @@ -494,4 +491,56 @@ Gegevens Niet-gekoppelde gegevens verwijderen Verwijdert bijlagen die in de database staan, maar niet aan een item zijn gekoppeld + Geeft de UUID weer die aan een item is gekoppeld + UUID tonen + Het opslaan van gegevens is niet toegestaan voor een database die is geopend als alleen-lezen. + Vraag om gegevens op te slaan wanneer een formulier is gevalideerd + Vragen om gegevens op te slaan + Probeer zoekinformatie op te slaan bij u een handmatige invoerselectie + Zoekinformatie opslaan + Sluit de database na een selectie voor automatisch aanvullen + Database sluiten + Schakel na het vergrendelen van de database automatisch terug naar het vorige toetsenbord + Database vergrendelen + Probeer gedeelde informatie op te slaan bij een handmatige invoerselectie + Gedeelde info opslaan + Melding + Biometrische beveiligingsupdate vereist. + Geen biometrische gegevens of apparaatgegevens geregistreerd. + Alles definitief uit de prullenbak verwijderen\? + Registratiemodus + Veilige modus + Zoekmodus + Het opslaan van een nieuw item is niet toegestaan in een alleen-lezen database + Koppel je wachtwoord aan je gescande biometrische gegevens of apparaatreferentie om je database snel te ontgrendelen. + Geavanceerde database-ontgrendeling + Enter + Backspace + Item selecteren + Terug naar vorig toetsenbord + Aangepaste velden + Alle coderingssleutels met betrekking tot geavanceerde ontgrendelingsherkenning verwijderen\? + Time-out voor geavanceerd ontgrendelen + Duur van geavanceerd ontgrendelingsgebruik voordat de inhoud wordt verwijderd + Vervaltijd voor geavanceerde ontgrendeling + Sla geen versleutelde inhoud op om geavanceerde ontgrendeling te gebruiken + Tijdelijke geavanceerde ontgrendeling + Hiermee kan je de referentie van je apparaat gebruiken om de database te openen + Ontgrendeling met apparaatreferenties + Tik om geavanceerde ontgrendelingstoetsen te verwijderen + Inhoud + Apparaatreferentie + Typ het wachtwoord en klik vervolgens op de knop \"Geavanceerd ontgrendelen\". + Kan geavanceerde ontgrendelingsprompt niet initialiseren. + Geavanceerde ontgrendelingsfout: %1$s + Kan geavanceerde ontgrendelingsafdruk niet herkennen + Kan de geavanceerde ontgrendelingssleutel niet lezen. Verwijder deze en herhaal de herkenningsprocedure voor het ontgrendelen. + Databasegegevens uitpakken met geavanceerde ontgrendelingsgegevens + Open database met geavanceerde ontgrendelingsherkenning + Waarschuwing: je moet nog steeds je hoofdwachtwoord onthouden als je geavanceerde ontgrendelingsherkenning gebruikt. + Geavanceerde ontgrendelingsherkenning + Open de geavanceerde ontgrendelingsprompt om inloggegevens op te slaan + Open de geavanceerde ontgrendelingsprompt om de database te ontgrendelen + Geavanceerde ontgrendelingssleutel verwijderen + De veldnaam bestaat al. \ No newline at end of file diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 19cc5d963..bd9511b38 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -25,7 +25,7 @@ Legg til gruppe Encryption algorithm Programtidsavbrot - Idle time before locking the database + App Programinnstillingar Parentesar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 753acdf16..6c9197cfe 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -338,8 +338,6 @@ ਡਾਟਾਬੇਸ ਬੰਦ ਕਰੋ ਲਿਖਣ ਤੋਂ ਸੁਰੱਖਿਅਤ ਐਂਟਰੀਆਂ ਵਿੱਚੋਂ ਲੱਭੋ - ਬਾਇਓਮੈਟਰਿਕ ਡਾਟਾਬੇਸ ਅਣ-ਲਾਕ ਕਰੋ - ਤੁਹਾਡੇ ਡਾਟਾਬੇਸ ਨੂੰ ਫ਼ੌਰੀ ਅਣ-ਲਾਕ ਕਰਨ ਲਈ ਆਪਣੇ ਪਾਸਵਰਡ ਨੂੰ ਆਪਣੇ ਸਕੈਨ ਕੀਤੇ ਬਾਇਮੈਟਰਿਕ ਨਾਲ ਲਿੰਕ ਕਰੋ। ਐਂਟਰੀ ਨੂੰ ਸੋਧੋ ਅਟੈਚਮੈਂਟ ਜੋੜੋ ਖ਼ਾਸ ਬਾਹਰੀ ਡਾਟਾ ਸੰਭਾਲਣ ਲਈ ਤੁਹਾਡੀ ਐਂਟਰੀ ਲਈ ਅਟੈਚਮੈਂਟ ਅੱਪਲੋਡ ਕਰੋ। diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index f9b10932b..309e9c809 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -175,7 +175,7 @@ Algorytm szyfrowania bazy danych używany dla wszystkich danych. Aby wygenerować klucz dla algorytmu szyfrowania, klucz główny jest transformowany przy użyciu losowo solonej funkcji wyprowadzania klucza. Użycie pamięci - Ilość pamięci (w bajtach) do użycia przez funkcję wyprowadzania klucza. + Ilość pamięci do użycia przez funkcję wyprowadzania klucza. Równoległy Stopień równoległości (tj. Liczba wątków) wykorzystywany przez funkcję wyprowadzania klucza. Sortuj @@ -218,7 +218,7 @@ Skanowanie odcisków palców Umożliwia zeskanowanie danych biometrycznych w celu otwarcia bazy danych Usuń klucze szyfrowania - Usuń wszystkie klucze szyfrowania związane z rozpoznawaniem linii papilarnych + Usuń wszystkie klucze szyfrujące związane z zaawansowanym rozpoznawaniem odblokowania Nie można uruchomić tej funkcji. Urządzenie pracuje na systemie Android %1$s, ale wymaga wersji %2$s lub nowszej. Nie można znaleźć odpowiedniego sprzętu. @@ -228,7 +228,7 @@ Utwórz nową bazę danych Wykorzystaj kosz Przenosi grupy i wpisy do grupy \"Kosz\" przed usunięciem - Pole czcionka + Czcionka pola Zmień czcionkę użytą w polach, aby poprawić widoczność postaci Zaufanie do schowka Zezwalanie na kopiowanie hasła wejściowego i chronionych pól do schowka @@ -237,7 +237,7 @@ Opis bazy danych Wersja bazy danych Tekst - Aplikacja + Wygląd Inne Klawiatura Magikeyboard @@ -261,8 +261,6 @@ \nGrupy (~ foldery) organizują wpisy w bazie danych. Przeszukuj wpisy Wprowadź tytuł, nazwę użytkownika lub zawartość innych pól, aby odzyskać swoje hasła. - Biometryczne odblokowanie bazy danych - Połącz swoje hasło z zeskanowanym odciskiem palca, aby szybko odblokować bazę danych. Edytuj wpis Edytuj swój wpis za pomocą pól niestandardowych. Dane puli mogą być przywoływane między różnymi polami wprowadzania. Utwórz silne hasło @@ -273,8 +271,8 @@ Zapisz ochronę swojej bazy danych Zmień tryb otwierania sesji. \n -\n„Zabezpieczony przed zapisem” zapobiega niezamierzonym zmianom w bazie danych. -\n„Modyfikowalne” pozwala dodawać, usuwać lub modyfikować wszystkie elementy. +\n\"Ochrona przed zapisem\" zapobiega niezamierzonych zmian w bazie danych. +\n\"Modyfikowalne\" pozwala dodawać, usuwać lub modyfikować wszystkie elementy, jak chcesz. Skopiuj pole Skopiowane pola można wkleić w dowolnym miejscu. \n @@ -295,12 +293,11 @@ zachęcasz programistów do tworzenia <strong>nowych funkcji</strong> i <strong>naprawy błędów</strong> zgodnie z Twoimi uwagami. Wielkie dzięki za twój wkład. Ciężko pracujemy, aby szybko udostępnić tę funkcję. - Nie zapomnij o aktualizacji aplikacji, instalując nowe wersje. + Pamiętaj, aby aktualizować swoją aplikację, instalując nowe wersje. Pobieranie Przyczyń się ChaCha20 AES - Argon2 Motyw aplikacji Motyw używany w aplikacji Pakiet ikon @@ -359,10 +356,10 @@ Nie można utworzyć bazy danych przy użyciu tego hasła i pliku klucza. Zaawansowane odblokowywanie Biometryczne - Automatyczne otwieranie monitu biometrycznego + Automatyczne otwieranie Włącz Wyłącz - Automatycznie pytaj o dane biometryczne, jeśli baza danych jest ustawiona do korzystania z nich + Automatyczne żądanie zaawansowanego odblokowania, jeśli baza danych jest skonfigurowana do korzystania z niej Węzły podrzędne Klucz główny Zabezpieczenia @@ -375,7 +372,7 @@ Cyfry Algorytm OTP - Nie można tutaj skopiować wpisu. + Tutaj nie można skopiować grupy. Tworzenie bazy danych… Ustawienia zabezpieczeń Ustawienia klucza głównego @@ -405,7 +402,7 @@ Użyj zaawansowanego odblokowywania w celu łatwiejszego otwierania bazy danych Kompresja danych zmniejsza rozmiar bazy danych Maksymalna liczba - Ogranicz rozmiar historii (w bajtach) na wpis + Ogranicz rozmiar historii na wpis Wymuś odnowienie Wymuś odnowienie następnym razem Wymagaj zmiany klucza głównego następnym razem (raz) @@ -506,7 +503,6 @@ Spróbuj zapisać udostępnione informacje podczas ręcznego wybierania pozycji Zapisz udostępnione informacje Powiadomienia - Nie można pobrać obiektu kryptograficznego. Wymagana aktualizacja zabezpieczeń biometrycznych. Nie zarejestrowano żadnych danych biometrycznych ani danych urządzenia. Trwale usunąć wszystkie węzły z kosza\? @@ -514,4 +510,46 @@ Tryb zapisywania Tryb wyszukiwania Zapisywanie nowego elementu nie jest dozwolone w bazie danych tylko do odczytu + Nazwa pola już istnieje. + Enter + Backspace + Wybierz pozycję + Wróć do poprzedniej klawiatury + Pola niestandardowe + Czy usunąć wszystkie klucze szyfrowania związane z zaawansowanym rozpoznawaniem odblokowania\? + Umożliwia otwieranie bazy danych za pomocą poświadczeń urządzenia + Odblokowanie uwierzytelniające urządzenia + Uwierzytelnienie urządzenia + Wpisz hasło, a następnie kliknij przycisk \"Zaawansowane odblokowanie\". + Nie można rozpoznać zaawansowanego wydruku odblokowującego + Nie można odczytać zaawansowanego klucza odblokowuj. Usuń go i powtórz procedurę rozpoznawania odblokowania. + Wyodrębnij poświadczenia bazy danych z zaawansowanymi danymi odblokowującymi + Ostrzeżenie: Jeśli używasz zaawansowanego rozpoznawania odblokowania, nadal musisz zapamiętać hasło główne. + Zaawansowane rozpoznawanie odblokowania + Usuń zaawansowany klucz odblokowujący + Połącz swoje hasło ze zeskanowanymi danymi biometrycznymi lub danymi logowania urządzenia, aby szybko odblokować bazę danych. + Zaawansowane odblokowywanie bazy danych + Limit czasu zaawansowanego odblokowywania + Czas trwania zaawansowanego odblokowywania przed usunięciem jego zawartości + Wygaśnięcie zaawansowanego odblokowania + Nie przechowuj żadnych zaszyfrowanych treści, aby korzystać z zaawansowanego odblokowywania + Naciśnij, aby usunąć zaawansowane klucze odblokowujące + Zawartość + Otwórz bazę danych z zaawansowanym rozpoznawaniem odblokowania + Argon2id + Argon2d + Błąd zaawansowanego odblokowywania: %1$s + Nie można poprawnie odbudować listy. + Nie można pobrać identyfikatora URI bazy danych. + Dodano sugestie autouzupełniania. + Sugestie wbudowane + Spróbuj wyświetlić sugestie autouzupełniania bezpośrednio z kompatybilnej klawiatury + Zaawansowane odblokowywanie tymczasowe + Nie można zainicjować monitu odblokowania zaawansowanego. + Otwórz monit odblokowania zaawansowanego, aby odblokować bazę danych + Otwórz monit odblokowania zaawansowanego, aby odblokować bazę danych + Dostęp do pliku odwołany przez menedżera plików, zamknij bazę danych i otwórz ją ponownie z jej lokalizacji. + Nadpisz zewnętrzne modyfikacje, zapisując bazę danych lub przeładuj ją z najnowszymi zmianami. + Informacje zawarte w pliku bazy danych zostały zmodyfikowane poza aplikacją. + Załaduj ponownie bazę danych \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 88d79de82..229432453 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -173,7 +173,7 @@ Algoritmo de encriptação usado para todos os dados. Para gerar a chave para o algoritmo de criptografia, a chave mestra é transformada usando uma função de derivação de chave com sal aleatoriamente. Uso de memória - Quantidade de memória (in bytes) a ser usada pela função de derivação de chave. + Quantidade de memória a ser usada pela função de derivação de chave. Paralelismo Grau de paralelismo (ou seja, número de threads) usado pela função de derivação de chave. Ordenar @@ -257,8 +257,6 @@ \nGrupos (~pastas) organizam suas entradas e sua base de dados. Pesquise suas entradas Entre com título, nome de usuário ou outros campos para recuperar facilmente suas senhas. - Desbloqueio do banco de dados por Impressão digital - Faça o link entre sua senha e sua impressão digital para rapidamente desbloquear seu banco de dados. Modifique a entrada Edite a sua entrada com campos personalizados. Os conjuntos de dados podem ser referenciados entre campos de entradas diferentes. Criar uma senha forte @@ -296,7 +294,6 @@ Contribuir ChaCha20 AES - Argon2 Tema do aplicativo Tema usado no aplicativo Pacote de ícones @@ -398,7 +395,7 @@ Número máximo Limitar o número de itens do histórico por entrada Tamanho máximo - Limitar o tamanho do histórico (em bytes) por entrada + Limitar o tamanho do histórico por entrada Renovação recomendada Recomende a mudança da chave mestra (em dias) Forçar renovação diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 59caf2e96..6557fdcc2 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -157,7 +157,7 @@ Média Grande - ASCII Estendido + ASCII Extendido Permitir Não foi possível abrir a sua base de dados. Não foi possível carregar a chave. Tente descarregar o \"Uso de Memória\" do KDF. @@ -174,7 +174,7 @@ Criar base de dados Para gerar uma chave para o algoritmo de encriptação, a chave mestre comprimida (SHA-256) é transformada usando uma função de derivação de chave (com um salt aleatório). Uso de memória - Quantidade de memória (em bytes) a ser usada pela função de derivação de chave. + Quantidade de memória a ser usada pela função de derivação de chave. Paralelismo Grau de paralelismo (ou seja, número de threads) usado pela função de derivação de chave. Ordenar @@ -247,8 +247,6 @@ \nGrupos (~pastas) organizam as suas entradas na sua base de dados. Pesquise suas entradas Entre com título, nome de utilizador ou outros campos para recuperar facilmente as suas palavras-passe. - Desbloqueio da base de dados por biométricos - Ligue a sua palavra-passe e o seu biométrico gravado para rapidamente desbloquear a sua base de dados. Modifique a entrada Edite a sua entrada com campos personalizados. Os conjuntos de dados podem ser referenciados entre campos de entradas diferentes. Crie uma palavra-passe forte @@ -283,7 +281,6 @@ Contribuir ChaCha20 AES - Argon2 Tema da app Tema usado na app Pacote de ícones @@ -301,7 +298,7 @@ \n \n\"Somente leitura\" evita que faça alterações não intencionais na base de dados. \n\"Gravação\" permite-o adicionar, apagar ou modificar todos os elementos. - Mostrar nomes de utilizador em listas de entrada + Mostrar nomes de utilizador na lista entradas Área de transferência Magikeyboard Magikeyboard (KeePassDX) @@ -402,7 +399,7 @@ Configurações de segurança Desativar Mostrar o botão de bloqueio na interface do utilizador - Limitar o tamanho do histórico (em bytes) por entrada + Limitar o tamanho do histórico por entrada A chave secreta deve estar em formato Base32. Ativar Compressão dos dados @@ -456,7 +453,7 @@ OTP Nenhum Gravar a base de dados automaticamente depois de uma ação importante (somente no modo \"Modificável\") - Mão pode copiar um grupo aqui. + Não pode copiar um grupo aqui. Apagar histórico Ao menos uma credencial deve ser definida. Forçar renovação @@ -494,4 +491,31 @@ \n \nA sua base de dados pode se tornar muito grande e reduzir o desempenho com este envio. Informações sobre credenciais + Credencial do dispositivo + Digite a palavra-chave, e depois clique no botão \"Desbloqueio avançado\". + Incapaz de inicializar o desbloqueio antecipado. + Erro de desbloqueio avançado: %1$s + Não conseguia reconhecer impressão de desbloqueio avançado + Não consegue ler a chave de desbloqueio avançada. Por favor, apague-a e repita o procedimento de desbloqueio de reconhecimento. + Extrair credencial de base de dados com dados de desbloqueio avançados + Base de dados aberta com reconhecimento avançado de desbloqueio + Advertência: Ainda precisa de se lembrar da sua palavra-passe principal se usar o reconhecimento avançado de desbloqueio. + Reconhecimento avançado de desbloqueio + Abrir o alerta de desbloqueio avançado para armazenar as credenciais + Abrir o alerta de desbloqueio avançado para desbloquear a base de dados + É necessária uma actualização de segurança biométrica. + O escaneamento biométrico é suportado, mas não configurado. + Acesso ao ficheiro revogado pelo gestor do ficheiro, fechar a base de dados e reabri-la a partir da sua localização. + Sobregravar as modificações externas, guardando a base de dados ou recarregando-a com as últimas alterações. + A informação contida no seu ficheiro de base de dados foi modificada fora da aplicação. + Apagar permanentemente todos os nós do caixote do lixo da reciclagem\? + Modo de registo + Modo Guardar + Modo de pesquisa + Apagar chave de desbloqueio avançada + Recarregar base de dados + Incapaz de reconstruir adequadamente a lista. + O URI da base de dados não pode ser recuperado. + O nome do campo já existe. + Salvar um novo item não é permitido numa base de dados só de leitura \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b19ab708d..948728c13 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,6 @@ Pacote de ícones Tema usado na app Tema da app - Argon2 AES ChaCha20 Twofish @@ -131,7 +130,7 @@ A gravar a base de dados… Grau de paralelismo (ou seja, número de threads) usado pela função de derivação de chave. Paralelismo - Quantidade de memória (em bytes) a ser usada pela função de derivação de chave. + Quantidade de memória a ser usada pela função de derivação de chave. Uso de memória Rodadas adicionais de criptografia adicionam mais proteção contra ataques de força bruta, mas podem tornar o processo de carregar e gravar mais lentos. Rodadas de criptografia @@ -194,7 +193,6 @@ Configurar OTP Envie um anexo à sua entrada para gravar dados externos importantes. Adicionar anexo - Desbloqueio da base de dados por biométricos Permite tocar no botão \"Abrir\" se nenhumas credenciais estiverem selecionadas Remove anexos contidos na base de dados, mas não ligados a uma entrada Remover dados não ligados @@ -290,7 +288,7 @@ Esvaziar cesto da reciclagem Gravar base de dados Não foi possível gravar a base de dados. - Ligue a sua palavra-passe e o seu biométrico gravado para rapidamente desbloquear a sua base de dados. + Ligue a sua palavra-passe e o seu biométrico gravado para rapidamente desbloquear a sua base de dados. Configurações do teclado do aparelho Ative um teclado customizado, populando suas palavras-passe e todos os campos de identidade Gzip @@ -304,7 +302,7 @@ Forçar renovação Recomendar mudança da chave mestre (em dias) Renovação recomendada - Limitar o tamanho do histórico (em bytes) por entrada + Limitar o tamanho do histórico por entrada Tamanho máximo Limitar a quantidade de itens do histórico por entrada Número máximo diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 2dafdff4d..a7d9acf4e 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -202,7 +202,7 @@ Rundele de transformare Rundele suplimentare de criptare oferă o protecție mai mare împotriva atacurilor de forță brută, dar pot încetini cu adevărat încărcarea și economisirea. Utilizarea memoriei - Cantitatea de memorie (în octeți) care trebuie utilizată de funcția de derivare a cheilor. + Cantitatea de memorie care trebuie utilizată de funcția de derivare a cheilor. Paralelism Gradul de paralelism (adică numărul de fire) utilizat de funcția de derivare a cheilor. Salvarea bazei de date … @@ -282,7 +282,7 @@ Număr maxim Limitați numărul de articole istorice pe intrare Dimensiune maximă - Limitați dimensiunea istoricului (în octeți) pe intrare + Limitați dimensiunea istoricului pe intrare Recomandă reînnoirea Recomandă schimbarea cheii master (zile) Forteaza reinoirea @@ -359,8 +359,6 @@ \nGrupuri (~ foldere) organizează intrări în baza de date. Căutați prin intrări Introduceți titlul, numele de utilizator sau conținutul altor câmpuri pentru a prelua parolele. - Deblocarea bazei de date prin biometric - Conectați parola la biometrica scanată pentru a debloca rapid baza de date. Modificați intrarea Modificați-vă înregistrarea cu câmpuri personalizate. Datele despre pool pot fi referențiate între diferite câmpuri de intrare. Creați o parolă puternică pentru intrarea dvs. @@ -408,7 +406,6 @@ Twofish ChaCha20 AES - Argon2 Tema aplicației Tema folosită în aplicație Pachet de pictograme diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 581c5166b..c9b312fc9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -95,7 +95,7 @@ Строчные Скрывать пароли Скрывать пароли за (***) по умолчанию - Сведения + О программе Изменить главный пароль Настройки Настройки базы @@ -168,7 +168,7 @@ Алгоритм шифрования базы для всех данных. При создании ключа для алгоритма шифрования, главный пароль преобразуется при помощи функции формирования ключа со случайной солью. Использование памяти - Объём памяти (в байтах), который будет использоваться функцией формирования ключа. + Объём памяти, который будет использоваться функцией формирования ключа. Уровень параллелизма Уровень параллелизма (т.е. количество потоков), используемый функцией формирования ключа. Сортировка @@ -214,13 +214,13 @@ Блокировка Блокировка экрана Блокировать базу при отключении экрана - Дополнительная разблокировка - Сканирование биометрического ключа + Расширенная разблокировка + Биометрическая разблокировка Включить разблокировку базы при помощи биометрического ключа Удалить ключи шифрования - Удалить все ключи шифрования, связанные с распознаванием биометрического ключа - Невозможно запустить эту функцию. - Ваша версия Android %1$s, но требуется %2$s. + Удалить все ключи шифрования, связанные с распознаванием расширенной разблокировки + Невозможно использовать эту функцию. + Ваша версия Android %1$s, требуется %2$s. Соответствующее оборудование не найдено. Имя файла Путь @@ -228,8 +228,8 @@ Создать новую базу Использовать \"корзину\" Перемещать группу или запись в \"корзину\" вместо удаления - Особый шрифт - Использовать в полях особый шрифт для лучшей читаемости + Особый шрифт полей + Использовать в полях специальный шрифт для лучшей читаемости Доверять буферу обмена Разрешить копирование пароля и защищённых полей в буфер обмена Внимание: буфер обмена доступен всем приложениям. Если копируются чувствительные данные, другие программы могут их перехватить. @@ -237,7 +237,7 @@ Описание базы Версия базы Текст - Приложение + Внешний вид Прочее Клавиатура Настройки Magikeyboard @@ -260,21 +260,19 @@ \n \nДобавляйте группы (аналог папок) для организации записей и баз. Легко находите ваши записи - Биометрическая разблокировка базы - Установите взаимосвязь между паролем и биометрическим ключом для быстрой разблокировки базы. Ищите записи по названию, имени или другим полям для быстрого доступа к своим паролям. Редактируйте записи - Редактируйте записи с настраиваемыми полями. Возможны перекрёстные ссылки между полями разных записей. + Редактируйте записи с пользовательскими полями. Возможны перекрёстные ссылки между полями разных записей. Создайте надёжный пароль Создайте надёжный пароль, связанный с записью, легко настраиваемый под критерии формы. И не забудьте главный пароль от базы. - Добавляйте настраиваемые поля + Добавляйте пользовательские поля Зарегистрируйте дополнительное поле, добавьте значение и при необходимости защитите его. Разблокируйте базу База только для чтения Изменяйте режим открытия в сессии. \n \nВ \"режиме только для чтения\" можно предотвратить непреднамеренные изменения в базе. -\nВ \"режиме записи\" вы можете добавлять, удалять или изменять любые элементы. +\nВ \"режиме записи\" можно добавлять, удалять или изменять любые элементы. Копируйте поля Скопированные поля можно вставить в любом месте. \n @@ -297,10 +295,9 @@ Мы прилагаем все усилия, чтобы быстро выпустить эту функцию. Не забывайте обновлять приложение. Скачать - Содействие + Помочь проекту ChaCha20 AES - Argon2 Тема приложения Тема, используемая в приложении Набор значков @@ -326,7 +323,7 @@ Звук при нажатии Не убивайте приложение… Блокировка нажатием \"Назад\" - Блокировка базы при нажатии кнопки \"Назад\" на начальном экране + Блокировка базы нажатием кнопки \"Назад\" на начальном экране Очищать при закрытии Блокировать базу при закрытии уведомления после использования или истечения времени ожидания Корзина @@ -357,10 +354,10 @@ Обновить Закрыть поля Невозможно создать базу с этим паролем и ключевым файлом. - Дополнительная разблокировка + Расширенная разблокировка Биометрический ключ - Автозапрос биометрического ключа - Автоматически запрашивать биометрический ключ, если он установлен для базы + Автозапрос ключа + Автоматически запрашивать расширенную разблокировку, если она установлена для базы Включить Отключить Режим выбора @@ -390,13 +387,13 @@ Исправить проблему путём создания новых UUID для дубликатов и продолжить работу\? База открыта Копирование полей ввода с помощью буфера обмена устройства - Использовать дополнительную разблокировку для более лёгкого открытия базы данных + Использовать расширенную разблокировку для более лёгкого открытия базы данных Сжатие данных Сжатие данных уменьшает размер базы Максимум записей Ограничение числа элементов истории каждой записи Максимальный размер - Ограничение размера истории каждой записи (в байтах) + Ограничение размера истории каждой записи Рекомендуемая смена Рекомендовать менять главный пароль (в днях) Принудительная смена @@ -497,7 +494,6 @@ Сохранять данные поиска Сохранять общую информацию при ручном выборе записи Сохранять общие данные - Невозможно получить доступ к зашифрованному объекту. Блокировка базы Автоматически переключаться на предыдущую клавиатуру после блокировки базы Показывать UUID, связанный с записью @@ -514,4 +510,46 @@ Режим записи Режим поиска Сохранение новых записей невозможно, т.к. база открыта только для чтения + Поле с таким именем уже существует. + Позволяет использовать учётные данные вашего устройства для открытия базы + Разблокировка учётными данными устройства + Учётные данные устройства + Введите пароль и нажмите кнопку \"Расширенная разблокировка\". + Невозможно инициализировать запрос расширенной разблокировки. + Невозможно распознать расширенную разблокировку + Невозможно прочитать ключ расширенной разблокировки. Удалите его и повторите процедуру распознавания разблокировки. + Извлекать учётные данные базы с использованием расширенной разблокировки + Открыть базу с расширенным распознаванием разблокировки + Предупреждение: даже при использовании расширенной разблокировки вам всё равно необходимо помнить главный пароль. + Открыть запрос расширенной разблокировки для сохранения учётных данных + Открыть запрос расширенной разблокировки для разблокировки базы + Удалить все ключи шифрования, связанные с распознаванием расширенной разблокировки\? + Ошибка расширенной разблокировки: %1$s + Распознавание расширенной разблокировки + Удалить ключ расширенной разблокировки + Ввод + Возврат к предыдущей клавиатуре + Пользовательские поля + Backspace + Выберите запись + Расширенная разблокировка базы + Срок действия расширенной разблокировки + Свяжите пароль с отсканированными биометрическими данными или учётными данными устройства, чтобы быстро разблокировать базу. + Продолжительность использования содержимого расширенной разблокировки до его удаления + Время действия + Временная расширенная разблокировка + Не сохранять зашифрованное содержимое для использования расширенной разблокировки + Нажмите, чтобы удалить ключи расширенной разблокировки + Содержимое + Argon2ID + Argon2D + Невозможно получить URI базы. + Невозможно правильно перестроить список. + Предложения автозаполнения добавлены. + Показывать предложения автозаполнения непосредственно в совместимой клавиатуре + Встроенные предложения + Доступ к файлу отозван файловым менеджером, закройте базу и снова откройте. + Сохранить базу, перезаписав внешние изменения, или перезагрузить её с последними изменениями. + Информация, содержащаяся в файле базы, была изменена вне этого приложения. + Перезагрузить базу \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 1ccb7debc..605a05c9b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . ---> - +--> Spätná väzba Domovská stránka Správca hesiel KeePass pre Android @@ -26,7 +25,7 @@ Pridať Skupinu Šifrovací algoritmus Časový limit aplikácie - Čas pred uzamknutím databázy, ak je aplikácia neaktívna. + Čas pred uzamknutím databázy, ak je aplikácia neaktívna Aplikácia Nastavenia aplikácie Konzoly diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index ef278761d..4683cecef 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -184,7 +184,7 @@ Krypteringsalgoritm som används för all data i databasen. För att generera nyckeln till krypteringsalgoritmen kommer huvudnyckeln transformeras med en slumpmässigt saltad nyckelderivatsfunktion. Minnesanvändning - Mängd minne (i bytes) som ska användas av nyckelderivatsfunktionen. + Mängd minne som ska användas av nyckelderivatsfunktionen. Parallellism Grad av parallellism (dvs. antal trådar) som används av nyckelderivatsfuntionen. Sortera @@ -281,7 +281,6 @@ Bidra ChaCha20 AES - Argon2 Apptema Tema som används i appen Ikonpaket @@ -393,7 +392,7 @@ Max. historikobjekt Begränsar antalet historikobjekt per post Max. historikstorlek - Begränsa historikens storlek per post (i bytes) + Begränsa historikens storlek per post Rekommenderas att ändra Rekommenderas att ändra huvudnyckeln (dagar) Tvingad ändring @@ -407,8 +406,6 @@ Gzip Aktivera ett anpassat tangentbord som innehåller dina lösenord och alla identitetsfält Enhetens tangentbordsinställningar - Lås upp databasen med biometrik - Länka dina lösenord till din skannande biometrik för att snabbt låsa upp din databas. Kunde inte spara databas. Spara databas Töm papperskorgen diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a8bf8c420..f72af2d2a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -21,8 +21,8 @@ Ana sayfa KeePass parola yöneticisinin Android uygulaması Kabul et - Girdi Ekle - Girdi Düzenle + Girdi ekle + Girdi düzenle Grup Ekle Şifreleme Şifreleme algoritması @@ -32,7 +32,7 @@ Uygulama Parantez Genişletilmiş ASCII - Veritabanı dosyaları oluşturmak, açmak ve kaydetmek için Niyet eylemini ACTION_CREATE_DOCUMENT ve ACTION_OPEN_DOCUMENT kabul eden bir dosya yöneticisi gerekir. + Niyet eylemini kabul eden bir dosya yöneticisi. ACTION_CREATE_DOCUMENT ve ACTION_OPEN_DOCUMENT veri tabanı dosyaları oluşturmak, açmak ve kaydetmek için gereklidir. İzin ver Pano temizlendi Pano hatası @@ -40,14 +40,14 @@ Pano temizlenemedi Pano zaman aşımı Panodaki depolama süresi (aygıtınız tarafından destekleniyorsa) - Veritabanı + Veri tabanı Erişildi İptal Notlar Parolayı onayla Oluşturuldu Değiştirilmiş - Giriş Verisi bulamadı. + Girdi verisi bulunamadı. Parola Kaydet Başlık @@ -58,9 +58,9 @@ Yolun doğru olduğundan emin olun. Bir isim girin. %1$s dosyasını panoya kopyalamak için seçin - Veritabanı anahtarı alınıyor… - Veritabanı içeriği deşifre ediliyor… - Öntanımlı veritabanı olarak kullan + Veri tabanı anahtarı alınıyor… + Veri tabanı içeriğinin şifresi çözülüyor… + Öntanımlı veri tabanı olarak kullan Rakamlar KeePassDX © %1$d Kunzisoft <strong>açık kaynaklıdır</strong> ve <strong>reklam içermez</strong>. \n<strong>GPLv3</strong> lisansı altında sağlanmaktadır, herhangi bir garanti vermez. @@ -69,8 +69,8 @@ Arcfour akış şifresi desteklenmiyor. Bu URI, KeePassDX\'te işlenemedi. Bir anahtar dosyası seçin. - Tüm veritabanınızı yüklemek için bellek yok. - Veritabanınız yüklenemedi. + Tüm veri tabanınızı yükleyecek kadar bellek yok. + Veri tabanınız yüklenemedi. Anahtar yüklenemedi. KDF \"Bellek Kullanımı\" nı azaltmaya çalışın. En az bir parola oluşturma türü seçilmelidir. Parolalar uyuşmuyor. @@ -93,14 +93,14 @@ Parola Kimlik bilgileri okunamadı. Yanlış algoritma. - Veritabanı biçimi tanımlanamadı. + Veri tabanı biçimi tanınamadı. Anahtar dosya boş. Uzunluk Kullanıcı adlarını göster - Giriş listelerinde kullanıcı adlarını göster + Girdi listelerinde kullanıcı adlarını göster Liste ögelerinin boyutu - Öğe listesindeki metin boyutu - Veritabanı yükleniyor… + Öge listesindeki metin boyutu + Veri tabanı yükleniyor… Küçük harf Parolaları gizle Parolaları öntanımlı olarak (***) ile maskele @@ -110,7 +110,7 @@ Ayarlar Uygulama ayarları Form doldurma - Veritabanı ayarları + Veri tabanı ayarları Bağış Yap Düzen Kopyala @@ -129,26 +129,26 @@ Asla Arama sonucu bulunamadı Bu URL\'u açmak için bir web tarayıcısı yükleyin. - Mevcut veritabanını aç - Yeni veritabanı oluştur - Yedek girişleri arama + Mevcut veri tabanını aç + Yeni veri tabanı oluştur + Yedek girdilerde arama Arama sonuçlarından \"Yedekleme\" ve \"Geri dönüşüm kutusu\" gruplarını atlar - Yeni veritabanı oluştur… + Yeni veri tabanı oluşturuluyor… Çalışıyor… Koruma Yazma korumalı - Veritabanınızdaki herhangi bir şeyi değiştirmek için KeePassDX\'in yazma iznine ihtiyacı var. + Dosya yöneticinize bağlı olarak KeePassDX\'in depolama alanınıza yazmasına izin verilmeyebilir. Kaldır Kök - Tüm veriler için veritabanı şifreleme algoritması kullanılmıştır. + Tüm veriler için kullanılan veri tabanı şifreleme algoritması. Şifreleme algoritmasının anahtarını üretmek için ana anahtar, rastgele anahtar türetme işlevi kullanılarak dönüştürülür. Dönüşüm turları Ek şifreleme turları, kaba kuvvet saldırılarına karşı daha yüksek koruma sağlar, ancak yükleme ve kaydetmeyi gerçekten yavaşlatabilir. Hafıza kullanımı - Anahtar türetme işlevi tarafından kullanılacak bellek miktarı (bayt olarak). + Anahtar türetme işlevi tarafından kullanılacak bellek miktarı. Paralellik Anahtar türev fonksiyonu tarafından kullanılan paralellik derecesi (yani iplik sayısı). - Veritabanı kaydediliyor… + Veri tabanı kaydediliyor… Boşluk Ara Sırala @@ -166,7 +166,7 @@ Arama Sonuçları Eksi Altı çizili - Desteklenmeyen veritabanı sürümü. + Desteklenmeyen veri tabanı sürümü. Büyük harf Uyarı Veri tabanı dosyasındaki metin kodlama biçiminin dışındaki parola karakterlerinden kaçın (tanınmayan karakterler aynı harfe dönüştürülür). @@ -175,7 +175,6 @@ Sürüm %1$s Yapı %1$s Şifreli parola saklandı - Bu veritabanının henüz bir parolası yok. Geçmiş Görünüm Genel @@ -190,16 +189,16 @@ İzin verilen parola üreticisi karakterlerini ayarla Pano Pano bildirimleri - Bir girişi görüntülerken alanları kopyalamak için pano bildirimlerini göster + Bir girdiyi görüntülerken alanları kopyalamak için pano bildirimlerini göster Panodaki otomatik silme başarısız olursa, geçmişini elle silin. Kilit Ekran kilidi - Ekran kapalıyken veritabanını kilitle - Parmakizi - Parmak izi tarama - Veritabanını açmak için biyometriklerinizi taramanızı sağlar + Ekran kapalıyken veri tabanını kilitle + Gelişmiş kilit açma + Biyometrik kilit açma + Veri tabanını açmak için biyometriklerinizi taramanızı sağlar Şifreleme anahtarlarını silin - Parmak izi tanıma ile ilgili tüm şifreleme anahtarlarını silin + Gelişmiş kilit açma tanıma ile ilgili tüm şifreleme anahtarlarını silin Bu özellik başlatılamadı. Aygıtta Android %1$s çalışıyor, ancak %2$s veya sonraki bir sürüm gerekli. İlgili donanım bulunamadı. @@ -208,16 +207,16 @@ Ana anahtar atayın Geri dönüşüm kutusu kullanımı Silmeden önce grupları ve girdileri \"Geri Dönüşüm Kutusu\"na taşır - Yazı tipi alanı + Alan yazı tipi Daha iyi karakter görünürlüğü için alanlarda kullanılan yazı tipini değiştirin Pano güveni - Giriş parolası ve korunan alanları panoya kopyalamaya izin ver + Girdi parolası ve korunan alanları panoya kopyalamaya izin ver Uyarı: Pano tüm uygulamalar tarafından paylaşılır. Hassas veriler kopyalanırsa, diğer yazılımlar bu verileri kurtarabilir. - Veritabanı adı - Veritabanı açıklaması - Veritabanı sürümü + Veri tabanı adı + Veri tabanı açıklaması + Veri tabanı sürümü Metin - Uygulama + Arayüz Diğer Klavye Magikeyboard @@ -227,14 +226,14 @@ Magikeyboard ayarları Girdi Zaman aşımı - Klavye girişini temizlemek için zaman aşımı + Klavye girdisini temizlemek için zaman aşımı Bildirim bilgisi - Bir giriş mevcut olduğunda bir bildirim göster + Bir girdi kullanılabilir olduğunda bildirim göster Girdi - Magikeyboard\'da %1$s mevcut + %1$s Magikeyboard\'da kullanılabilir %1$s Kapanışta temizle - Bildirimi kapatırken veritabanını kapatın + Bildirimi kapatırken veri tabanını kapat Görünüm Klavye teması Anahtarlar @@ -249,42 +248,40 @@ Eğitim ipuçlarını sıfırla Tüm eğitim bilgilerini tekrar göster Eğitim ipuçlarını sıfırla - Veritabanı dosyanızı oluşturun + Veri tabanı dosyanızı oluşturun İlk parola yönetim dosyanızı oluşturun. - Mevcut bir veritabanını aç - Kullanmaya devam etmek için önceki veritabanı dosyanızı dosya tarayıcınızdan açın. + Mevcut bir veri tabanını açın + Kullanmaya devam etmek için önceki veri tabanı dosyanızı dosya tarayıcınızdan açın. Veri tabanınıza öge ekleyin - Girdiler dijital kimliğinizi yönetmenize yardımcı olur. -\n -\nGruplar (~klasörler) veritabanınızdaki girdileri düzenler. - Girişlerde ara - Parolanızı kurtarmak için başlık, kullanıcı adı veya diğer alanların içeriğini girin. - Parmak iziyle veri tabanı kilidi açma - Veritabanınızı hızlıca açmak için parolanızı taranan parmak izinize bağlayın. - Girdiyi düzenle - Girdinizi özel alanlarla düzenleyin. Havuz verileri farklı giriş alanları arasında referans alınabilir. - Güçlü bir parola oluşturun - Girişinizle ilişkilendirmek için güçlü bir parola oluşturun, formun kriterlerine göre kolayca tanımlayın ve güvenli parolayı unutmayın. - Özel alanlar ekle - Ek bir alan kaydedin, bir değer ekleyin ve isteğe bağlı olarak koruyun. - Veritabanınızın kilidini açın - Veritabanınızın kilidini açmak için parola ve/veya anahtar dosya girin. + Girdiler dijital kimliğinizi yönetmenize yardımcı olur. \n -\nHer değişiklikten sonra veritabanı dosyanızı güvenli bir yerde yedekleyin. - Veritabanınızı yazmaya karşı koru +\nGruplar (~klasörler) veri tabanınızdaki girdileri düzenler. + Girdilerde arayın + Parolanızı kurtarmak için başlık, kullanıcı adı veya diğer alanların içeriğini girin. + Girdiyi düzenleyin + Girdinizi özel alanlarla düzenleyin. Havuz verileri farklı girdi alanları arasında referans alınabilir. + Güçlü bir parola oluşturun + Girdinizle ilişkilendirmek için güçlü bir parola oluşturun, formun kriterlerine göre kolayca tanımlayın ve güvenli parolayı unutmayın. + Özel alanlar ekleyin + Ek bir alan kaydedin, bir değer ekleyin ve isteğe bağlı olarak koruyun. + Veri tabanınızın kilidini açın + Veri tabanınızın kilidini açmak için parola ve/veya anahtar dosyası girin. +\n +\nHer değişiklikten sonra veri tabanı dosyanızı güvenli bir yerde yedekleyin. + Veri tabanınızı yazmaya karşı koruyun Oturum için açılış modunu değiştir. \n -\n\"Yazma korumalı\", veritabanında istenmeyen değişiklikleri önler. -\n\"Değiştirilebilir\", tüm öğeleri eklemenizi, silmenizi veya değiştirmenizi sağlar. - Bir alan kopyala +\n\"Yazma korumalı\", veri tabanında istenmeyen değişiklikleri önler. +\n\"Değiştirilebilir\", tüm ögeleri istediğiniz gibi eklemenize, silmenize veya değiştirmenize izin verir. + Bir alanı kopyalayın Kopyalanan alanlar herhangi bir yere yapıştırılabilir. \n \nTercih ettiğiniz form doldurma yöntemini kullanın. - Veritabanını kilitle - Veritabanınızı hızlıca kilitleyin, uygulamayı bir süre sonra kilitlemek için ve ekran kapandığında ayarlayabilirsiniz. - Öge sıralama + Veri tabanını kilitleyin + Veri tabanınızı hızlıca kilitleyin, belirli bir süre sonra ve ekran kapandığında kilitlenmesi için uygulamayı ayarlayabilirsiniz. + Ögeleri sıralayın Girdilerin ve grupların nasıl sıralandığını seçin. - Katıl + Katılın Daha fazla özellik ekleyerek istikrarı, güvenliği artırmaya yardımcı olun. Birçok parola yönetimi uygulamasının aksine, bu uygulama <strong>reklam içermez</strong>, <strong> copyleft lisanslı özgür yazılımdır</strong> ve hangi sürümü kullanırsanız kullanın, sunucularında kişisel veri toplamaz. Profesyonel sürümü satın alarak, bu <strong>görsel stile</strong> erişebilecek ve özellikle <strong>topluluk projelerinin gerçekleştirilmesine</strong> yardımcı olacaksınız. @@ -294,7 +291,7 @@ <strong>Pro</strong> sürümü satın alarak, <strong>Katkıda</strong> bulunarak, geliştiricilerin <strong>yeni özellikler</strong> oluşturmasını ve söz konusu hatalara göre <strong>hataları düzeltmesini</strong> teşvik ediyorsunuz. - Katkınız için çok teşekkür ederim. + Katkıda bulunduğunuz için çok teşekkür ederim. Bu özelliği çabucak yayınlamak için çok çalışıyoruz. Yeni sürümleri yükleyerek uygulamanızı güncel tutmayı unutmayın. İndir @@ -303,37 +300,36 @@ Twofish ChaCha20 AES - Argon2 Uygulama teması Uygulamada kullanılan tema Simge paketi Uygulamada kullanılan simge paketi Seçim modu Uygulamayı öldürmeyin… - Kullanıcı kök ekranda geri düğmesine tıkladığında veritabanını kilitle + Kullanıcı kök ekranda geri düğmesine tıkladığında veri tabanını kilitle Kapanışta temizle Panonun süresi dolduğunda veya kullanmaya başladıktan sonra bildirim kapatıldığında veri tabanını kilitle Dosya aç Düğüm ekle - Girdi Ekle + Girdi ekle Grup ekle Dosya bilgileri Parola onay kutusu Anahtar dosyası onay kutusu - Giriş simgesi + Girdi simgesi Parola üreteci Parola uzunluğu Alan ekle Alanı kaldır UUID - Buraya bir girişi taşıyamazsınız. - Buraya bir girişi kopyalayamazsınız. - Giriş sayısını göster - Bir gruptaki girişlerin sayısını göster + Bir girdiyi buraya taşıyamazsınız. + Bir girdiyi buraya kopyalayamazsınız. + Girdi sayısını göster + Bir gruptaki girdilerin sayısını göster Kilitlemek için \'Geri\'ye basın Geri dönüşüm kutusu - Giriş seçimi - Bir girişi görüntülerken Magikeyboard\'da giriş alanlarını göster + Girdi seçimi + Bir girdiyi görüntülerken Magikeyboard\'da giriş alanlarını göster Parolayı sil Veri tabanına bağlantı denemesinden sonra girilen parolayı siler Alt düğüm @@ -341,11 +337,11 @@ Arka plan Güncelleme Alanları kapat - Bu parola ve anahtar dosyası ile veritabanı oluşturulamıyor. + Bu parola ve anahtar dosyası ile veri tabanı oluşturulamıyor. Gelişmiş kilit açma Biyometrik - Biyometrik taramayı otomatik aç - Vari tabanı, onu kullanacak biçimde ayarlandıysa biyometriği otomatik olarak sor + Otomatik açma istemi + Vari tabanı onu kullanacak biçimde ayarlandıysa otomatik olarak gelişmiş kilit açma isteğinde bulun Etkinleştir Devre dışı Ana anahtar @@ -367,20 +363,20 @@ Periyot %1$d ile %2$d saniye arasında olmalıdır. Belirteç %1$d ile %2$d basamak içermelidir. %1$s aynı UUID değerine sahip %2$s zaten var. - Veritabanı oluşturuluyor… + Veri tabanı oluşturuluyor… Güvenlik ayarları Ana anahtar ayarları - Veritabanı tekrarlanan UUID\'ler içermektedir. + Veri tabanı tekrarlanan UUID\'ler içeriyor. Tekrarlananlar için yeni UUID\'ler oluşturarak sorunu çöz ve devam et\? - Veritabanı açıldı - Aygıtınızın panosunu kullanarak giriş alanlarını kopyala - Veritabanını daha kolay açmak için gelişmiş kilit açma özelliğini kullan + Veri tabanı açıldı + Aygıtınızın panosunu kullanarak girdi alanlarını kopyalayın + Veri tabanını daha kolay açmak için gelişmiş kilit açma özelliğini kullan Veri sıkıştırma Veri sıkıştırma veri tabanı boyutunu azaltır Azami sayı Girdi başına geçmiş ögelerinin sayısını sınırla Azami boyut - Giriş başına geçmiş boyutunu (bayt olarak) sınırlama + Girdi başına geçmiş boyutunu sınırla Yenilemeyi öner Ana anahtarın değiştirilmesini öner (gün) Yenilemeyi zorla @@ -388,19 +384,19 @@ Bir dahaki sefere yenilemeye zorla Bir dahaki sefere ana anahtarı değiştirmeyi gerektirir (bir kez) Öntanımlı kullanıcı adı - Özel veritabanı rengi + Özel veri tabanı rengi Sıkıştırma Yok Gzip Aygıt klavye ayarları - Veritabanı kaydedilemedi. + Veri tabanı kaydedilemedi. Veri tabanını kaydet Geri dönüşüm kutusunu boşalt Komut çalıştırılıyor… Seçilen düğümler kalıcı olarak silinsin mi\? Anahtar deposu düzgün bir şekilde başlatılmadı. Geri dönüşüm kutusu grubu - Veritabanını otomatik kaydet + Veri tabanını otomatik kaydet Her önemli işlemden sonra veri tabanını kaydet (\"Değiştirilebilir\" modda) Ekler Geçmişi geri yükle @@ -415,9 +411,9 @@ Süresi dolmuş girdileri gizle Süresi dolmuş girdiler gösterilmez Veri tabanı değişikliklerini kaydetmek için dosya yazma erişimi ver - Son veritabanları listesindeki bozuk bağlantıları gizle - Bozuk veritabanı bağlantılarını gizle - Son veritabanlarının konumlarını göster + Son veri tabanları listesindeki bozuk bağlantıları gizle + Bozuk veri tabanı bağlantılarını gizle + Son veri tabanlarının konumlarını göster Son dosyaları göster Anahtar dosyalarının nerede saklandığını takip eder Anahtar dosyası konumlarını hatırla @@ -428,8 +424,8 @@ Katkı İletişim İki ögeli kimlik doğrulaması (2FA) için istenen bir belirteç oluşturmak için bir kerelik parola yönetimini (HOTP / TOTP) ayarlayın. - OTP ayarla - Veritabanı dosyası oluşturulamıyor. + Bir kerelik parola (OTP) ayarlayın + Veri tabanı dosyası oluşturulamıyor. <strong>Özgürlüğümüzü korumak</strong>, <strong>hataları düzeltmek</strong>, <strong>özellikler eklemek</strong> ve <strong>her zaman etkin olmak</strong> için, <strong>desteğinize</strong> güveniyoruz. Ek ekle Vazgeç @@ -455,14 +451,14 @@ Alt etki alanı arama Bu metin istenen ögeyle eşleşmiyor. Öge ekle - Otomatik tuş eylemini gerçekleştirdikten sonra otomatik olarak önceki klavyeye dön + \"Otomatik tuş eylemini\" gerçekleştirdikten sonra otomatik olarak önceki klavyeye dön Otomatik tuş eylemi Veri tabanı kimlik bilgileri ekranında otomatik olarak önceki klavyeye dön Veri tabanı kimlik bilgileri ekranı Klavye değiştir %1$s yükle Önemli harici verileri kaydetmek için girdinize bir ek yükleyin. - Ek ekle + Ek ekleyin Dosya yine de eklensin mi\? Bu dosyanın yüklenmesi mevcut dosyanın yerini alacaktır. Bir KeePass veri tabanının sadece küçük yardımcı dosyaları (PGP anahtar dosyaları gibi) içermesi beklenmektedir. @@ -490,7 +486,6 @@ Elle girdi seçimi yaparken paylaşılan bilgileri kaydetmeyi dene Paylaşılan bilgileri kaydet Bildirim - Şifreleme nesnesi alınamıyor. Biyometrik güvenlik güncellemesi gerekli. Biyometrik bilgiler veya aygıt kimlik doğrulama bilgileri kaydedilmedi. Geri dönüşüm kutusundaki tüm düğümler kalıcı olarak silinsin mi\? @@ -498,4 +493,47 @@ Kaydetme modu Arama modu Salt okunur bir veri tabanında yeni bir öge kaydetmeye izin verilmiyor + Alan adı zaten var. + Gelişmiş kilit açma tanıma ile ilgili tüm şifreleme anahtarları silinsin mi\? + Veri tabanını açmak için aygıt kimlik bilgilerinizi kullanmanıza olanak tanır + Aygıt kimlik bilgisiyle kilit açma + Aygıt kimlik bilgileri + Parolayı yazın ve ardından \"Gelişmiş kilit açma\" düğmesine tıklayın. + Gelişmiş kilit açma istemi başlatılamıyor. + Bu veri tabanında henüz saklanmış kimlik bilgisi yok. + Gelişmiş kilit açma hatası: %1$s + Gelişmiş kilit açma parmak izi tanınamadı + Gelişmiş kilit açma anahtarı okunamıyor. Lütfen silin ve kilit açma tanıma işlemini tekrarlayın. + Veri tabanı kimlik bilgilerini gelişmiş kilit açma özelliğiyle çıkarın + Veri tabanını gelişmiş kilit açma tanıma ile aç + Uyarı: Gelişmiş kilit açma tanıma kullanırsanız, yine de ana parolanızı hatırlamanız gerekmektedir. + Gelişmiş kilit açma tanıma + Kimlik bilgilerini saklamak için gelişmiş kilit açma istemini aç + Veri tabanının kilidini açmak için gelişmiş kilit açma istemini aç + Gelişmiş kilit açma anahtarını sil + Enter + Backspace + Girdi seç + Önceki klavyeye geri dön + Özel alanlar + Veri tabanınızın kilidini hızlı bir şekilde açmak için parolanızı taranmış biyometrik veya aygıt kimlik bilgilerinize bağlayın. + Gelişmiş veri tabanı kilidi açma + Gelişmiş kilit açma zaman aşımı + İçeriğini silmeden önce gelişmiş kilit açma kullanımının süresi + Gelişmiş kilit açma süre sonu + Gelişmiş kilit açmayı kullanmak için herhangi bir şifrelenmiş içeriği saklama + Geçici gelişmiş kilit açma + Gelişmiş kilit açma anahtarlarını silmek için dokunun + İçerik + Argon2id + Argon2d + Liste düzgün şekilde yeniden oluşturulamıyor. + Veri tabanı URI\'si alınamıyor. + Otomatik doldurma önerileri eklendi. + Doğrudan uyumlu bir klavyeden otomatik doldurma önerileri görüntülemeye çalış + Satır içi öneriler + Dosyaya erişim dosya yöneticisi tarafından iptal edildi, veri tabanını kapatın ve bulunduğu yerden yeniden açın. + Veri tabanını kaydederek veya en son değişikliklerle yeniden yükleyerek harici değişikliklerin üzerine yazın. + Veri tabanı dosyanızda bulunan bilgiler, uygulamanın dışında değiştirildi. + Veri tabanını yeniden yükle \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c81b85438..e8c80deca 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -25,11 +25,11 @@ Додати групу Алгоритм шифрування Час очікування застосунку - Час очікування до блокування бази даних + Час бездіяльности до блокування бази даних Застосунок Параметри застосунку Дужки - Для створення, відкриття та збереження файлів баз даних потрібен файловий менеджер, який приймає дії Intent ACTION_CREATE_DOCUMENT та ACTION_OPEN_DOCUMENT. + Для створення, відкриття та збереження файлів баз даних потрібен файловий менеджер, який приймує дії Intent ACTION_CREATE_DOCUMENT та ACTION_OPEN_DOCUMENT. Буфер обміну очищено Час до очищення буфера обміну Тривалість зберігання в буфері обміну (якщо підтримується пристроєм) @@ -66,7 +66,7 @@ Необхідно вибрати принаймні один тип створення пароля. Паролі не збігаються. Забагато циклів. Встановлено 2147483648. - Введіть додатне ціле число до поля \"Довжина\". + Введіть додатне ціле додатне число до поля \"Довжина\". Файловий менеджер Згенерувати пароль Підтвердити пароль @@ -154,7 +154,7 @@ Gzip Стиснення Інше - Застосунок + Інтерфейс Текст Версія бази даних Власний колір бази даних @@ -206,7 +206,7 @@ Поле файла ключа Налаштування головного ключа Параметри безпеки - Розширені параметри розблокування + Розширене розблокування Заповнення форм Скопійовано %1$s Створення бази даних… @@ -227,7 +227,7 @@ Не вдалося зберегти базу даних. Неможливо створити базу даних із цим паролем та файлом ключа. Не вдалося створити файл бази даних. - Ви не можете скопіювати групу сюди. + Ви не можете копіювати групу сюди. Ви не можете копіювати записи сюди. Ви не можете перемістити запис сюди. Ви не можете перемістити групу в себе саму. @@ -271,7 +271,6 @@ Набір піктограм Тема застосунку Тема застосунку - Argon2 AES ChaCha20 Готово! @@ -281,7 +280,7 @@ Завантаження %1$s Допомогти Завантажити - Не забудьте постійно оновлювати застосунок, встановлюючи нові версії. + Не забувайте оновлювати застосунок, встановлюючи нові версії. Ми докладаємо всіх зусиль для якнайшвидшого випуску цієї функції. Щиро вдячні за вашу допомогу. ви заохочуєте розробників створювати <strong>нові функції</strong> та <strong>виправляти помилки</strong> відповідно до ваших зауважень. @@ -305,7 +304,7 @@ Змініть режим сеансу. \n \n\"Захист від запису\" запобігає внесенню випадкових змін до бази даних. -\n\"Дозволити зміни\" дозволяє вам додавати, видаляти чи змінювати будь-які елементи. +\n\"Дозволити зміни\" дозволяє додавати, видаляти чи змінювати будь-які елементи. Зберігати базу даних після кожної важливої дії (у режимі \"Дозволити зміни\") Захистіть вашу базу даних від запису Розблокуйте вашу базу даних @@ -317,8 +316,6 @@ Створіть надійний пароль Додавайте власні поля до записів. Дані полів різних записів можна повʼязувати між собою. Змінюйте записи - Зв\'яжіть пароль та скановані біометричні дані для швидкого розблокування бази даних. - Біометричне розблокування бази даних Введіть назву, ім\'я користувача або вміст інших полів для пошуку паролів. Шукайте поміж записів Створюйте записи для керування вашими обліковими записами. @@ -355,7 +352,7 @@ %1$s доступне на Magikeyboard Запис Час очікування до очищення клавіатури від запису - Час очікування + Обмеження часу Закрити базу даних під час закриття сповіщення Очищати під час закриття Показувати сповіщення, коли запис доступний @@ -380,7 +377,7 @@ Примусова зміна Рекомендувати змінити головний ключ (днів) Рекомендувати змінити - Обмежити об\'єм історії (в байтах) кожного запису + Обмежити об\'єм історії кожного запису Максимальний об\'єм Обмежити кількість елементів історії кожного запису Максимальна кількість @@ -394,17 +391,17 @@ Назва файлу Пристрій працює під керуванням Android %1$s, але необхідний %2$s чи пізніші. Не вдалось запустити цю функцію. - Видалити всі ключі шифрування, пов\'язані з розпізнаванням біометричного ключа + Видалити всі ключі шифрування, пов\'язані з розпізнаванням розширеного розблокування Видалити ключі шифрування - Автоматично запитувати біометричний ключ, якщо базу даних налаштовано для роботи з ним - Автоматично запитувати біометричний ключ + Автоматично запитувати розширене розблокування, якщо базу даних налаштовано для роботи з ним + Автозапит ключа Дозволяє сканувати біометричні дані, щоб відкрити базу даних Розблокування біометричним ключем Увімкніть розширені параметри розблокування та спростіть відкриття бази даних Розширені параметри розблокування Показувати кнопку блокування в інтерфейсі користувача Показувати кнопку блокування - Блокувати базу даних, коли користувач натискає кнопку назад на головному екрані + Блокувати базу даних, коли користувач натисне кнопку назад на головному екрані Натисніть \'Назад\', щоб заблокувати Блокувати базу даних, якщо екран вимкнено Блокування екрана @@ -450,7 +447,7 @@ Спочатку групи За зростанням ↓ Впорядкувати - Об\'єм пам\'яті (у байтах), необхідний для функції створення ключа. + Об\'єм пам\'яті, необхідний для функції створення ключа. Для створення ключа алгоритму шифрування, головний ключ перетворюється за допомогою функції створення випадково приготованого ключа. Не закривайте застосунок… Виконання команди… @@ -472,8 +469,8 @@ Фільтр Додати елемент Перемикання клавіатури - Автоматично перемикатися до попередньої клавіатури після виконання дії Самочинного введення - Самочинне введення + Автоматично перемикатися до попередньої клавіатури після виконання дії «Автоматична дія кнопки» + Автоматична дія кнопки Автоматичне перемикання до попередньої клавіатури, на екрані входу до бази даних Екран входу до бази даних База даних KeePass має містити лише невеликі файли утиліт (наприклад, файли ключів PGP). @@ -506,7 +503,6 @@ Намагатися зберегти спільні відомості під час вибору запису власноруч Збереження спільних відомостей Сповіщення - Не вдалося отримати крипто-об\'єкт. Необхідно оновити біометричний захист. Біометричних чи облікових даних пристрою не зареєстровано. Видалити всі вузли з кошика остаточно\? @@ -514,4 +510,46 @@ Режим збереження Режим пошуку Збереження нового елемента заборонено в базі даних лише для читання + Назва поля вже існує. + Видалити всі ключі шифрування, пов\'язані з розширеним розпізнаванням розблокування\? + Дає змогу використовувати облікові дані пристрою для відкриття бази даних + Розблокування облікових даних пристрою + Облікові дані пристрою + Введіть пароль, а потім натисніть кнопку «Розширене розблокування». + Не вдалося ініціалізувати запит на розширене розблокування. + Помилка розширеного розблокування: %1$s + Не вдалося розпізнати розширене розблокування + Не вдалося розпізнати ключ розширеного розблокування. Видаліть його та повторіть процедуру створення ключа. + Витягування облікових даних бази даних за допомогою даних розширеного розблокування + Відкрити базу даних розширеним розпізнаванням розблокування + Попередження: Навіть якщо ви користуєтеся розширеним розблокуванням, вам однаково необхідно пам\'ятати головний пароль. + Розпізнавання розширеного розблокування + Відкривати запит розширеного розблокування, щоб зберегти облікові дані + Відкривати запит розширеного розблокування, щоб розблокувати базу даних + Видалити ключ розширеного розблокування + Enter + Backspace + Вибрати запис + Повернутися до попередньої клавіатури + Власні поля + Час дії розширеного розблокування + Час дії розширеного розблокування + Пов’яжіть свій пароль зі сканованими біометричними даними або даними пристрою, щоб швидко розблокувати базу даних. + Розширене розблокування бази даних + Тривалість використання розширеного розблокування перед видаленням його вмісту + Не зберігайте зашифрований вміст для використання розширеного розблокування + Тимчасове розширене розблокування + Торкнутися, щоб видалити клавіші розширеного розблокування + Вміст + Argon2id + Argon2d + Не вдалося належним чином відновити список. + Неможливо отримати URI бази даних. + Додано пропозиції автозаповнення. + Спробувати показ пропозицій автозаповнення безпосередньо з сумісної клавіатури + Вбудовані пропозиції + Доступ до файлу скасовано менеджером файлів, закрийте базу даних і знову відкрийте її з її розташування. + Перезаписати зовнішні зміни, зберігши базу даних або перезавантажте її з найновішими змінами. + Відомості, що містяться у файлі бази даних, змінено за межами застосунку. + Перезавантажити базу даних \ No newline at end of file diff --git a/app/src/main/res/values-v23/donottranslate.xml b/app/src/main/res/values-v23/donottranslate.xml index e761117a7..e1e50e325 100644 --- a/app/src/main/res/values-v23/donottranslate.xml +++ b/app/src/main/res/values-v23/donottranslate.xml @@ -19,4 +19,5 @@ --> true + true diff --git a/app/src/main/res/values-v30/donottranslate.xml b/app/src/main/res/values-v30/donottranslate.xml new file mode 100644 index 000000000..fc50571b3 --- /dev/null +++ b/app/src/main/res/values-v30/donottranslate.xml @@ -0,0 +1,22 @@ + + + + true + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4bc8c9d24..f6d58bb79 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -29,7 +29,7 @@ 应用 程序设置 括号 - 新建与保存数据库需要支持打开和新建文件的文件管理器。 + 需要一款接受意图操作 ACTION_CREATE_DOCUMENT 和 ACTION_OPEN_DOCUMENT 的文件管理器来创建、打开和保存数据库文件。 剪贴板已清空 剪贴板清空延时 剪贴板保存时间 (若您的设备支持该功能) @@ -145,7 +145,6 @@ 无法清空剪切板 ChaCha20 AES - Argon2 主题 图标包 程序中使用的图标包 @@ -179,7 +178,7 @@ 加密所有数据时采用的算法。 将迭代主密钥以生成加密数据库所需的密钥,转换方式为随机加盐算法。 内存使用量 - 密钥推导算法使用的内存(以二进制字节计)。 + 密钥推导算法使用的内存。 并行 密钥推导算法使用的并行运行程度(即线程数量)。 排序 @@ -208,7 +207,7 @@ 数据库描述 数据库版本 文字样式 - 程序 + 界面 其他 键盘 魔法键盘 @@ -243,14 +242,14 @@ 生物识别解锁 通过生物识别解锁数据库 删除加密密钥 - 删除所有与生物识别相关的密钥 + 删除所有与高级解锁识别相关的加密密钥 无法启动此功能。 此设备运行 Android %1$s ,但应用需要 %2$s 或更高版本。 找不到所需的硬件。 分配主密钥 回收站使用情况 删除群组和条目前先移至回收站 - 字段字体 + 字段字型 更改字段字体,可以使字符更清楚 允许复制整段密码和受保护的字段至剪切板 警告:复制密码时密码在剪贴板中,而所有程序都可访问剪切板。因此复制密码时,设备上的其他程序也能看到密码。 @@ -291,8 +290,6 @@ \n还可以使用群组来管理数据库中的条目。 搜索条目 输入标题、用户名或其他字段的内容来搜索密码。 - 正在解锁生物识别数据库 - 将主密钥与生物识别信息关联,以快速解锁数据库。 编辑此条目 使用自定义字段编辑条。自定义字段可以在不同的条目间引用。 新建一个强密码 @@ -301,10 +298,10 @@ 添加一个新的字段并添加为其添加一个值,此时可以选择是否保护该字段及其值。 解锁数据库 数据库启用写入保护(只读) - 更改会话的打开模式。 -\n -\n“写入保护(只读)”可防止对数据库的意外更改。 -\n“可编辑(可写)”允许您添加、删除或者修改元素。 + 更改此会话的打开模式。 +\n +\n“写入保护(只读)”可防止对数据库的意外更改。 +\n“可编辑”允许您随心所欲地添加、删除或者修改所有元素。 复制字段 已复制的字段可以粘贴到任何地方。 \n @@ -325,11 +322,11 @@ 您的留言,是对开发人员添加<strong>新功能</strong>及<strong>修复 bugs</strong> 的鼓励。 非常感谢您的捐助和贡献。 我们正在努力的研发并尽快发布新特性。 - 别忘了更新程序。 + 别忘了通过安装新版本来保持你的应用程序是最新的。 选择模式 不要终止程序… 按返回键以锁定 - 点击屏幕底部的返回键时锁定数据库 + 当用户单击根屏幕上的返回按钮时锁定数据库 关闭程序时清空剪贴板 剪贴板持续时间过期或通知在您开始使用后关闭时,锁定数据库 回收站 @@ -362,8 +359,8 @@ 无法使用此密码和密钥文件新建数据库。 高级解锁 生物识别 - 自动打开生物识别提示 - 生物识别密钥已配置时自动打开提示 + 自动打开提示 + 自动请求高级解锁,如果数据库设置为使用它 启用 禁用 主密钥 @@ -379,7 +376,7 @@ 一次性密码 错误的一次性密码密钥。 至少需要设置一个凭据。 - 不能将群组复制到这里。 + 这里不能复制组。 密钥必须是BASE32格式。 计数器必须在%1$d和%2$d之间。 时长必须在%1$d秒到%2$d秒之间。 @@ -398,7 +395,7 @@ 最大数量 限制每个条目的历史记录条数 最大大小 - 限制每个条目的历史记录大小(以字节为单位) + 限制每个条目的历史记录大小 建议修改 建议修改主密钥(以天为单位) 强制修改 @@ -472,7 +469,7 @@ 文本和请求的条目不匹配. 添加条目 数据库凭据屏幕 - 执行自动键动作后,自动切换回前一个键盘 + 执行“自动键动作”后,自动切换回前一个键盘 自动键动作 如果显示数据库凭据屏幕,则自动返回到上一个键盘 切换键盘 @@ -506,7 +503,6 @@ 手动选择条目时,尝试保存共享信息 保存分享的信息 通知 - 无法检索加密对象。 需要生物识别安全更新。 未登记生物识别或设备凭证。 从回收站永久删除所有节点? @@ -514,4 +510,46 @@ 保存模式 搜索模式 只读数据库不允许保存新条目 + 字段名已经存在。 + 删除所有与高级解锁识别相关的加密密钥吗? + 允许您使用设备凭据来打开数据库 + 设备凭据解锁 + 设备凭据 + 输入密码,然后点击“高级解锁”按钮。 + 无法初始化高级解锁提示。 + 高级解锁出错:%1$s + 无法识别高级解锁印记 + 无法读取高级解锁密钥。请删除它,并重复解锁识别步骤。 + 用高级解锁数据提取数据库凭据 + 用高级解锁识别打开数据库 + 警告:即使您使用高级解锁识别,您仍然需要记住您的主密码。 + 高级解锁识别 + 打开高级解锁提示来存储凭证 + 打开高级解锁提示来解锁数据库 + 删除高级解锁密钥 + 输入 + 退格键 + 选择条目 + 回到先前的键盘 + 自定义字段 + 将您的密码连接到您扫描的生物特征或设备凭据,以快速解锁您的数据库。 + 高级数据库解锁 + 高级解锁超时 + 删除内容之前高级解锁使用的持续时间 + 高级解锁过期 + 不要存储任何加密内容来使用高级解锁 + 临时性高级解锁 + 点击删除高级解锁密钥 + 内容 + Argon2id + Argon2d + 无法正确地重建列表。 + 无法检索数据库 URI 。 + 已添加自动填充建议。 + 尝试直接从兼容的键盘显示自动填充建议 + 内联建议 + 文件管理器撤销了对此文件的访问,关闭数据库并从其位置重新打开它。 + 通过保存数据库或用最新的更改重新加载数据库来覆盖外部修改。 + 数据库文件中包含的信息已在应用程序之外被修改。 + 重新加载数据库 \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index bfa602c58..299a25928 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -94,11 +94,15 @@ advanced_unlock_explanation_key biometric_unlock_enable_key false - biometric_auto_open_prompt_key - false - biometric_delete_all_key_key device_credential_unlock_enable_key false + biometric_auto_open_prompt_key + false + temp_advanced_unlock_enable_key + false + temp_advanced_unlock_timeout_key + 36000000 + biometric_delete_all_key_key settings_form_filling_key @@ -147,6 +151,8 @@ false autofill_auto_search_key true + autofill_inline_suggestions_key + false autofill_save_search_info_key true autofill_ask_to_save_data_key @@ -175,7 +181,7 @@ true hide_expired_entries_key false - hide_expired_entries_key + show_uuid_key false enable_education_screens_key true @@ -287,6 +293,20 @@ 1800000 -1 + + 300000 + 900000 + 1800000 + 3600000 + 7200000 + 18000000 + 36000000 + 86400000 + 172800000 + 604800000 + 2592000000 + -1 + 32dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94d50d479..3aaa72ef0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with KeePassDX. If not, see . ---> - +--> Contact Contribution Feedback @@ -37,7 +36,7 @@ App Brackets Extended ASCII - A file manager that accepts the Intent action ACTION_CREATE_DOCUMENT and ACTION_OPEN_DOCUMENT is needed to create, open and save database files. + A file manager that accepts the ACTION_CREATE_DOCUMENT and ACTION_OPEN_DOCUMENT intent action is required to create, open, and save database files. Allow Clipboard cleared Clipboard error @@ -119,12 +118,12 @@ \"Transformation rounds\" too high. Setting to 2147483648. Each string must have a field name. This label already exists. - Enter a positive whole number in the \"Length\" field. + Enter a positive integer number in the \"Length\" field. Could not enable autofill service. You can not move a group into itself. You can not move an entry here. You can not copy an entry here. - You can not copy a group here. + You cannot copy a group here. Unable to create database file. Unable to create database with this password and keyfile. Could not save database. @@ -132,9 +131,12 @@ Counter must be between %1$d and %2$d. Period must be between %1$d and %2$d seconds. Token must contain %1$d to %2$d digits. + The existing OTP type is not recognized by this form, its validation may no longer correctly generate the token. This text does not match the requested item. Saving a new item is not allowed in a read-only database The field name already exists. + Database URI cannot be retrieved. + Unable to properly rebuild the list. Field name Field value Could not find file. Try reopening it from your file browser. @@ -184,6 +186,7 @@ Hide password Lock database Save database + Reload database Open Search Show password @@ -231,7 +234,7 @@ Transformation rounds Additional encryption rounds provide higher protection against brute force attacks, but can really slow down loading and saving. Memory usage - Amount of memory (in bytes) to be used by the key derivation function. + Amount of memory to be used by the key derivation function. Parallelism Degree of parallelism (i.e. number of threads) used by the key derivation function. Saving database… @@ -271,6 +274,9 @@ Remove this data anyway? It is not recommended to add an empty keyfile. The content of the keyfile should never be changed, and in the best case, should contain randomly generated data. + The information contained in your database file has been modified outside the app. + Overwrite the external modifications by saving the database or reload it with the latest changes. + Access to the file revoked by the file manager, close the database and reopen it from its location. Version %1$s Build %1$s No biometric or device credential is enrolled. @@ -288,7 +294,6 @@ Advanced unlock error: %1$s This database does not have stored credential yet. Unable to initialize advanced unlock prompt. - Unable to retrieve crypto object. Type in the password, and then click the \"Advanced unlock\" button. History Appearance @@ -318,7 +323,9 @@ Lock the database when the user clicks the back button on the root screen Show lock button Displays the lock button in the user interface + Content Advanced unlock + Tap to delete advanced unlocking keys Use advanced unlocking to open a database more easily Biometric unlocking Lets you scan your biometric to open the database @@ -326,6 +333,11 @@ Lets you use your device credential to open the database Auto-open prompt Automatically request advanced unlock if the database is set up to use it + Temp advanced unlocking + Do not store any encrypted content to use advanced unlocking + Advanced unlocking expiration + Duration of advanced unlocking usage before deleting its content + Advanced unlocking timeout Delete encryption keys Delete all encryption keys related to advanced unlock recognition Delete all encryption keys related to advanced unlock recognition? @@ -346,14 +358,14 @@ Maximum number Limit the number of history items per entry Maximum size - Limit the history size (in bytes) per entry + Limit the history size per entry Recommend renewal Recommend changing the master key (days) Force renewal Require changing the master key (days) Force renewal next time Require changing the master key the next time (once) - Field font + Field typeface Change font used in fields for better character visibility Clipboard trust Allow copying the entry password and protected fields to the clipboard @@ -369,7 +381,7 @@ Custom database color Database version Text - App + Interface Other Compression None @@ -412,10 +424,17 @@ Automatically switch back to the previous keyboard after executing \"Auto key action\" Lock database Automatically switch back to the previous keyboard after locking the database + Custom fields + Back to previous keyboard + Select entry + Backspace + Enter Close database Close the database after an autofill selection Auto search Automatically suggest search results from the web domain or application ID + Inline suggestions + Attempt to display autofill suggestions directly from a compatible keyboard Save search info Try to save search information when making a manual entry selection Ask to save data @@ -427,6 +446,7 @@ Block autofill Restart the app containing the form to activate the blocking. Data save is not allowed for a database opened as read-only. + Autofill suggestions added. Allow no master key Allows tapping the \"Open\" button if no credentials are selected Delete password @@ -448,8 +468,8 @@ Entries help manage your digital identities.\n\nGroups (~folders) organize entries in your database. Search through entries Enter title, username or content of other fields to retrieve your passwords. - Biometric database unlocking - Link your password to your scanned biometric to quickly unlock your database. + Advanced database unlocking + Link your password to your scanned biometric or device credential to quickly unlock your database. Edit the entry Edit your entry with custom fields. Pool data can be referenced between different entry fields. Create a strong password @@ -463,7 +483,10 @@ Unlock your database Enter the password and/or keyfile to unlock your database.\n\nBackup your database file in a safe place after each change. Write protect your database - Change opening mode for the session.\n\n\"Write-protected\" prevents unintended changes to the database.\n\"Modifiable\" lets you add, delete, or modify all elements. + Change opening mode for the session. +\n +\n\"Write-protected\" prevents unintended changes to the database. +\n\"Modifiable\" lets you add, delete, or modify all the elements as you wish. Copy a field Copied fields can be pasted anywhere.\n\nUse the form filling method you prefer. Lock the database @@ -482,7 +505,7 @@ you are encouraging developers to create <strong>new features</strong> and to <strong>fix bugs</strong> according to your remarks. Thanks a lot for your contribution. We are working hard to release this feature quickly. - Do not forget to keep your app up to date by installing new versions. + Remember to keep your app up to date by installing new versions. Download Contribute Download %1$s @@ -495,7 +518,12 @@ Twofish ChaCha20 AES - Argon2 + Argon2d + Argon2id + B + KiB + MiB + GiB 5 seconds 10 seconds @@ -507,6 +535,20 @@ 30 minutes Never + + 5 minutes + 15 minutes + 30 minutes + 1 hour + 2 hours + 5 hours + 10 hours + 24 hours + 48 hours + 1 week + 1 month + Never + Small Medium @@ -529,4 +571,4 @@ Expired entries are not shown Show UUID Displays the UUID linked to an entry - + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 76406e20a..ca871ad06 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -465,6 +465,7 @@ @android:color/transparent @null true + true true false diff --git a/app/src/main/res/xml/dataset_service.xml b/app/src/main/res/xml/dataset_service.xml index ea2597ec7..6d595b7b5 100644 --- a/app/src/main/res/xml/dataset_service.xml +++ b/app/src/main/res/xml/dataset_service.xml @@ -23,7 +23,9 @@ Settings Activity. This is pointed to in the service's meta-data in the applicat + android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity" + android:supportsInlineSuggestions="true" + tools:ignore="UnusedAttribute"> diff --git a/app/src/main/res/xml/keyboard_password.xml b/app/src/main/res/xml/keyboard_password.xml index d6c8cfbb7..50e128b21 100644 --- a/app/src/main/res/xml/keyboard_password.xml +++ b/app/src/main/res/xml/keyboard_password.xml @@ -18,6 +18,8 @@ --> @@ -26,26 +28,30 @@ + android:isRepeatable="false" /> diff --git a/app/src/main/res/xml/keyboard_password_entry.xml b/app/src/main/res/xml/keyboard_password_entry.xml index 566974b13..72dcbe389 100644 --- a/app/src/main/res/xml/keyboard_password_entry.xml +++ b/app/src/main/res/xml/keyboard_password_entry.xml @@ -18,6 +18,8 @@ --> @@ -26,25 +28,36 @@ android:codes="520" android:keyEdgeFlags="left" android:keyIcon="@drawable/ic_link_black_24dp" - android:keyWidth="24%p" + android:tooltipText="@string/entry_url" + android:keyWidth="15%p" android:horizontalGap="1%p" android:isRepeatable="false"/> + @@ -54,6 +67,7 @@ diff --git a/app/src/main/res/xml/preferences_advanced_unlock.xml b/app/src/main/res/xml/preferences_advanced_unlock.xml index 9ce2402fd..0b66ceefb 100644 --- a/app/src/main/res/xml/preferences_advanced_unlock.xml +++ b/app/src/main/res/xml/preferences_advanced_unlock.xml @@ -39,6 +39,23 @@ android:title="@string/biometric_auto_open_prompt_title" android:summary="@string/biometric_auto_open_prompt_summary" android:defaultValue="@bool/biometric_auto_open_prompt_default"/> + + + + + diff --git a/app/src/main/res/xml/preferences_database.xml b/app/src/main/res/xml/preferences_database.xml index 151bbb57d..c4fabd5dc 100644 --- a/app/src/main/res/xml/preferences_database.xml +++ b/app/src/main/res/xml/preferences_database.xml @@ -101,7 +101,7 @@ android:title="@string/max_history_items_title" android:positiveButtonText="@string/save" android:negativeButtonText="@string/entry_cancel"/> - - + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/art/ic_fingerprint_notification.svg b/art/ic_fingerprint_notification.svg new file mode 100644 index 000000000..aaa185bc0 --- /dev/null +++ b/art/ic_fingerprint_notification.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index cde378402..8e5371d69 100644 --- a/build.gradle +++ b/build.gradle @@ -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() diff --git a/fastlane/metadata/android/en-US/changelogs/47.txt b/fastlane/metadata/android/en-US/changelogs/47.txt index 0310fcc24..87a5613f5 100644 --- a/fastlane/metadata/android/en-US/changelogs/47.txt +++ b/fastlane/metadata/android/en-US/changelogs/47.txt @@ -1 +1,6 @@ - * Unlock database by device credentials (PIN/Password/Pattern) #779 #102 \ No newline at end of file + * Unlock database by device credentials (PIN/Password/Pattern) #779 #102 + * Advanced unlock with timeout #102 #437 #566 + * Remove default database parameter when the file is no longer accessible #803 + * Move OTP button to the first view level in Magikeyboard #587 + * Tooltips for Magikeyboard #586 + * Fix small bugs #805 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/48.txt b/fastlane/metadata/android/en-US/changelogs/48.txt new file mode 100644 index 000000000..d5ae30391 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/48.txt @@ -0,0 +1,2 @@ + * Fix small bugs #812 + * Argon2ID implementation #791 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/49.txt b/fastlane/metadata/android/en-US/changelogs/49.txt new file mode 100644 index 000000000..af4f00858 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/49.txt @@ -0,0 +1,3 @@ + * Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811 + * Prevent auto switch back to previous keyboard if otp field exists #814 + * Fix timeout reset #817 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/50.txt b/fastlane/metadata/android/en-US/changelogs/50.txt new file mode 100644 index 000000000..457eb4856 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/50.txt @@ -0,0 +1 @@ + * Fix KeyFile bug #820 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/51.txt b/fastlane/metadata/android/en-US/changelogs/51.txt new file mode 100644 index 000000000..25c598fe9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/51.txt @@ -0,0 +1,2 @@ + * Fix small bugs + * Remove write permission since Android 10 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/52.txt b/fastlane/metadata/android/en-US/changelogs/52.txt new file mode 100644 index 000000000..5edd71c36 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/52.txt @@ -0,0 +1,2 @@ + * Fix specific attachments with kdbx3.1 databases #828 + * Fix small bugs \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/53.txt b/fastlane/metadata/android/en-US/changelogs/53.txt new file mode 100644 index 000000000..f9ef200fe --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/53.txt @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/54.txt b/fastlane/metadata/android/en-US/changelogs/54.txt new file mode 100644 index 000000000..b96a30ff4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/54.txt @@ -0,0 +1,2 @@ + * Try to fix autofill #852 + * Fix database change dialog displayed too often #853 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/55.txt b/fastlane/metadata/android/en-US/changelogs/55.txt new file mode 100644 index 000000000..65ebabbb8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/55.txt @@ -0,0 +1,2 @@ + * Add Keyfile XML version 2 (fix hex) #844 + * Fix hex Keyfile #861 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/56.txt b/fastlane/metadata/android/en-US/changelogs/56.txt new file mode 100644 index 000000000..60621bf81 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/56.txt @@ -0,0 +1,6 @@ + * Fix OTP token type #863 + * Fix auto open biometric prompt #862 + * Fix back appearance setting #865 + * Fix orientation change in settings #872 + * Change memory unit to MiB #851 + * Small changes #642 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/57.txt b/fastlane/metadata/android/en-US/changelogs/57.txt new file mode 100644 index 000000000..e9c7c3f47 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/57.txt @@ -0,0 +1,3 @@ + * Fix TOTP plugin settings #878 + * Allow Emoji #796 + * Scroll and better UI in entry edition screen #876 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/47.txt b/fastlane/metadata/android/fr-FR/changelogs/47.txt index 5c7e79bd4..4491102fa 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/47.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/47.txt @@ -1 +1,6 @@ - * Déverouillage de base de données avec identifiants de l'appareil (PIN/Password/Pattern) #779 #102 \ No newline at end of file + * Déverouillage de base de données avec identifiants de l'appareil (PIN/Password/Pattern) #779 #102 + * Déverouillage avancé avec expiration #102 #437 #566 + * Supprimer le parmètre de base de données par défaut quand le fichier n'est plus accessible #803 + * Déplacer le bouton OTP vers le premier niveau de vue dans le Magiclavier #587 + * Info-bulles pour le Magiclavier #586 + * Correction petits bugs #805 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/48.txt b/fastlane/metadata/android/fr-FR/changelogs/48.txt new file mode 100644 index 000000000..a04fb074b --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/48.txt @@ -0,0 +1,2 @@ + * Correction de petits bugs #812 + * Implementation d'Argon2ID #791 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/49.txt b/fastlane/metadata/android/fr-FR/changelogs/49.txt new file mode 100644 index 000000000..da28437c5 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/49.txt @@ -0,0 +1,3 @@ + * Déverouillage de base de données avec identifiants de l'appareil (PIN/Password/Pattern) pour Android M+ #102 #152 #811 + * Empêcher le retour automatique au clavier précédent si le champ otp existe #814 + * Correction de la réinitialisation du timer #817 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/50.txt b/fastlane/metadata/android/fr-FR/changelogs/50.txt new file mode 100644 index 000000000..45cc3d57d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/50.txt @@ -0,0 +1 @@ + * Correction du bug de fichier de clé #820 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/51.txt b/fastlane/metadata/android/fr-FR/changelogs/51.txt new file mode 100644 index 000000000..18233cde2 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/51.txt @@ -0,0 +1,2 @@ + * Correction de petits bugs + * Suppression des permissions d'écriture à partir d'Android 10 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/52.txt b/fastlane/metadata/android/fr-FR/changelogs/52.txt new file mode 100644 index 000000000..c3ebc1548 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/52.txt @@ -0,0 +1,2 @@ + * Correction des pièces jointes spécifiques avec les bases kdbx3.1 #828 + * Correction de petits bugs \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/53.txt b/fastlane/metadata/android/fr-FR/changelogs/53.txt new file mode 100644 index 000000000..b6ec26240 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/53.txt @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/54.txt b/fastlane/metadata/android/fr-FR/changelogs/54.txt new file mode 100644 index 000000000..e11d58628 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/54.txt @@ -0,0 +1,2 @@ + * Tentative de résolution de l'autofill #852 + * Correction du dialogue de changement de base de données affiché trop souvent #853 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/55.txt b/fastlane/metadata/android/fr-FR/changelogs/55.txt new file mode 100644 index 000000000..fff6228b0 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/55.txt @@ -0,0 +1,2 @@ + * Ajout de fichier de clé XML version 2 (correction hex) #844 + * Correction de fichier de clé hex #861 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/56.txt b/fastlane/metadata/android/fr-FR/changelogs/56.txt new file mode 100644 index 000000000..61fec4409 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/56.txt @@ -0,0 +1,6 @@ + * Correction du type de token OTP #863 + * Correction de l'ouverture automatique de l'invite biométrique #862 + * Correction du retour dans les paramètres d'apparence #865 + * Correction du changement d'orientation dans les paramètres #872 + * Modification des unités de mémoire en MiB #851 + * Petits changements #642 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/57.txt b/fastlane/metadata/android/fr-FR/changelogs/57.txt new file mode 100644 index 000000000..8885da30c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/57.txt @@ -0,0 +1,3 @@ + * Correction des paramètres des plugins TOTP #878 + * Autoriser les Emoji #796 + * Défilement et amélioration de l'UI dans l'écran d'édition #876 \ No newline at end of file diff --git a/fastlane/pro/metadata/android/en-US/full_description.txt b/fastlane/pro/metadata/android/en-US/full_description.txt index 144c9e3c7..9ecd13423 100644 --- a/fastlane/pro/metadata/android/en-US/full_description.txt +++ b/fastlane/pro/metadata/android/en-US/full_description.txt @@ -1,7 +1,7 @@ Multi-format KeePass password manager, 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 faster development, better service, and you contribute to the creation of open source softwares without advertising. -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. +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. Features - Create database files / entries and groups. diff --git a/fastlane/pro/metadata/android/fr-FR/full_description.txt b/fastlane/pro/metadata/android/fr-FR/full_description.txt index 2969463ba..5342ba8db 100644 --- a/fastlane/pro/metadata/android/fr-FR/full_description.txt +++ b/fastlane/pro/metadata/android/fr-FR/full_description.txt @@ -1,7 +1,7 @@ Gestionnaire de mots de passe KeePass multiformats, 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 un développement plus rapide, un meilleur service et vous contribuez à la création de logiciels open source sans publicité. -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. +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. Fonctionnalités - Création de bases de données / entrées et groupes. diff --git a/fastlane/pro/metadata/android/ja-JP/full_description.txt b/fastlane/pro/metadata/android/ja-JP/full_description.txt index 2ff47dfee..d6f7237e4 100644 --- a/fastlane/pro/metadata/android/ja-JP/full_description.txt +++ b/fastlane/pro/metadata/android/ja-JP/full_description.txt @@ -1,7 +1,7 @@ 複数の形式に対応する KeePass パスワード マネージャー。Android の設計基準が組み込まれており、パスワード、鍵、デジタル ID を安全な方法で保存して使用できます。 この pro バージョンは開発中です。購入することで開発の加速サービスの改善を支援し、広告なしのオープンソース ソフトウェアの作成に貢献できます。 -現在、このアプリケーションの機能はテーマのロックが解除された free バージョンと同じです。よく利用されるサイトやサービスの接続と同期を楽にする要素を統合することが予定されています。 +現在、このアプリケーションの機能はテーマのロックが解除された free バージョンと同じです。一般的に使われている不自由なサイトやサービスに関連する要素を統合することを計画しています。 機能 - データベースファイル / エントリー・グループの作成