Merge branch 'feature/Mnemonics' into develop

This commit is contained in:
J-Jamet
2022-04-01 19:14:07 +02:00
42 changed files with 2439 additions and 720 deletions

View File

@@ -1,3 +1,11 @@
KeePassDX(3.4.0)
* Show visual password strength indicator with entropy #631 #869
* Dynamically save password generator configuration #618 #696
* Add advanced password filters #1052
* Add editable chars fields #539
* Add color for special password chars #454
* Passphrase implementation #218
KeePassDX(3.3.3)
* Fix shared otpauth link if database not open #1274
* Ellipsize attachment name #1253

View File

@@ -127,6 +127,8 @@ dependencies {
// Apache Commons
implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15'
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
// Encrypt lib
implementation project(path: ':crypto')
// Icon pack

View File

@@ -131,6 +131,9 @@
<activity
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
android:configChanges="keyboardHidden" />
<activity
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
android:configChanges="keyboardHidden" />

View File

@@ -58,9 +58,12 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.*
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
@@ -78,11 +81,9 @@ import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import org.joda.time.DateTime
import java.util.*
import kotlin.collections.ArrayList
class EntryEditActivity : DatabaseLockActivity(),
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
@@ -119,6 +120,20 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.selectIcon(icon)
}
private var mPasswordField: Field? = null
private var mKeyGeneratorResultLauncher = KeyGeneratorActivity.registerForGeneratedKeyResult(this) { keyGenerated ->
keyGenerated?.let {
mPasswordField?.let {
it.protectedValue.stringValue = keyGenerated
mEntryEditViewModel.selectPassword(it)
}
}
mPasswordField = null
Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
@@ -268,9 +283,8 @@ class EntryEditActivity : DatabaseLockActivity(),
}
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
GeneratePasswordDialogFragment
.getInstance(passwordField)
.show(supportFragmentManager, "PasswordGeneratorFragment")
mPasswordField = passwordField
KeyGeneratorActivity.launch(this, mKeyGeneratorResultLauncher)
}
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
@@ -656,17 +670,6 @@ class EntryEditActivity : DatabaseLockActivity(),
mEntryEditViewModel.selectTime(hours, minutes)
}
override fun acceptPassword(passwordField: Field) {
mEntryEditViewModel.selectPassword(passwordField)
Handler(Looper.getMainLooper()).post {
performedNextEducation()
}
}
override fun cancelPassword(passwordField: Field) {
// Do nothing here
}
override fun onBackPressed() {
onApprovedBackPressed {
super@EntryEditActivity.onBackPressed()

View File

@@ -31,7 +31,6 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi

View File

@@ -0,0 +1,139 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorActivity : DatabaseLockActivity() {
private lateinit var toolbar: Toolbar
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var validationButton: View
private var lockView: View? = null
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_key_generator)
toolbar = findViewById(R.id.toolbar)
toolbar.title = " "
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
coordinatorLayout = findViewById(R.id.key_generator_coordinator)
lockView = findViewById(R.id.lock_button)
lockView?.setOnClickListener {
lockAndExit()
}
validationButton = findViewById(R.id.key_generator_validation)
validationButton.setOnClickListener {
keyGeneratorViewModel.validateKeyGenerated()
}
supportFragmentManager.commit {
replace(R.id.key_generator_fragment, KeyGeneratorFragment.getInstance(
// Default selection tab
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD
), KEY_GENERATED_FRAGMENT_TAG
)
}
keyGeneratorViewModel.keyGenerated.observe(this) { keyGenerated ->
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(KEY_GENERATED, keyGenerated)
})
finish()
}
}
override fun viewToInvalidateTimeout(): View? {
return findViewById<ViewGroup>(R.id.key_generator_container)
}
override fun onResume() {
super.onResume()
// Show the lock button
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
View.VISIBLE
} else {
View.GONE
}
// Padding if lock button visible
toolbar.updateLockPaddingLeft()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.key_generator, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
}
R.id.menu_generate -> {
keyGeneratorViewModel.requireKeyGeneration()
}
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
setResult(Activity.RESULT_CANCELED, Intent())
super.onBackPressed()
}
companion object {
private const val KEY_GENERATED = "KEY_GENERATED"
private const val KEY_GENERATED_FRAGMENT_TAG = "KEY_GENERATED_FRAGMENT_TAG"
fun registerForGeneratedKeyResult(activity: FragmentActivity,
keyGeneratedListener: (String?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
keyGeneratedListener.invoke(
result.data?.getStringExtra(KEY_GENERATED)
)
} else {
keyGeneratedListener.invoke(null)
}
}
}
fun launch(context: FragmentActivity,
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon
resultLauncher.launch(
Intent(context, KeyGeneratorActivity::class.java)
)
}
}
}

View File

@@ -1,224 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.applyFontVisibility
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
private var mListener: GeneratePasswordListener? = null
private var root: View? = null
private var lengthTextView: EditText? = null
private var passwordInputLayoutView: TextInputLayout? = null
private var passwordView: EditText? = null
private var mPasswordField: Field? = null
private var uppercaseBox: CompoundButton? = null
private var lowercaseBox: CompoundButton? = null
private var digitsBox: CompoundButton? = null
private var minusBox: CompoundButton? = null
private var underlineBox: CompoundButton? = null
private var spaceBox: CompoundButton? = null
private var specialsBox: CompoundButton? = null
private var bracketsBox: CompoundButton? = null
private var extendedBox: CompoundButton? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
mListener = context as GeneratePasswordListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + GeneratePasswordListener::class.java.name)
}
}
override fun onDetach() {
mListener = null
super.onDetach()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
val inflater = activity.layoutInflater
root = inflater.inflate(R.layout.fragment_generate_password, null)
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
passwordView = root?.findViewById(R.id.password)
passwordView?.applyFontVisibility()
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(activity)
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
lengthTextView = root?.findViewById(R.id.length)
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
digitsBox = root?.findViewById(R.id.cb_digits)
minusBox = root?.findViewById(R.id.cb_minus)
underlineBox = root?.findViewById(R.id.cb_underline)
spaceBox = root?.findViewById(R.id.cb_space)
specialsBox = root?.findViewById(R.id.cb_specials)
bracketsBox = root?.findViewById(R.id.cb_brackets)
extendedBox = root?.findViewById(R.id.cb_extended)
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
assignDefaultCharacters()
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
lengthTextView?.setText(progress.toString())
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
context?.let { context ->
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
}
root?.findViewById<Button>(R.id.generate_password_button)
?.setOnClickListener { fillPassword() }
builder.setView(root)
.setPositiveButton(R.string.accept) { _, _ ->
mPasswordField?.let { passwordField ->
passwordView?.text?.toString()?.let { passwordValue ->
passwordField.protectedValue.stringValue = passwordValue
}
mListener?.acceptPassword(passwordField)
}
dismiss()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
mPasswordField?.let { passwordField ->
mListener?.cancelPassword(passwordField)
}
dismiss()
}
// Pre-populate a password to possibly save the user a few clicks
fillPassword()
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun assignDefaultCharacters() {
uppercaseBox?.isChecked = false
lowercaseBox?.isChecked = false
digitsBox?.isChecked = false
minusBox?.isChecked = false
underlineBox?.isChecked = false
spaceBox?.isChecked = false
specialsBox?.isChecked = false
bracketsBox?.isChecked = false
extendedBox?.isChecked = false
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
for (passwordChar in charSet) {
when (passwordChar) {
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
}
}
}
}
}
private fun fillPassword() {
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
}
fun generatePassword(): String {
var password = ""
try {
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
password = PasswordGenerator(resources).generatePassword(length,
uppercaseBox?.isChecked == true,
lowercaseBox?.isChecked == true,
digitsBox?.isChecked == true,
minusBox?.isChecked == true,
underlineBox?.isChecked == true,
spaceBox?.isChecked == true,
specialsBox?.isChecked == true,
bracketsBox?.isChecked == true,
extendedBox?.isChecked == true)
passwordInputLayoutView?.error = null
} catch (e: NumberFormatException) {
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
} catch (e: IllegalArgumentException) {
passwordInputLayoutView?.error = e.message
}
return password
}
interface GeneratePasswordListener {
fun acceptPassword(passwordField: Field)
fun cancelPassword(passwordField: Field)
}
companion object {
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
fun getInstance(field: Field): GeneratePasswordDialogFragment {
return GeneratePasswordDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_PASSWORD_FIELD, field)
}
}
}
}
}

View File

@@ -36,8 +36,11 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
@@ -48,8 +51,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var passwordCheckBox: CompoundButton? = null
private var passwordTextInputLayout: TextInputLayout? = null
private var passwordView: TextView? = null
private var passKeyView: PassKeyView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
@@ -59,6 +61,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mListener: AssignMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null
private var mPasswordEntropyCalculator: PasswordEntropy? = null
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
private var mNoKeyConfirmationDialog: AlertDialog? = null
@@ -100,6 +103,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onDetach()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create the password entropy object
mPasswordEntropyCalculator = PasswordEntropy()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
@@ -123,10 +133,10 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
passwordView = rootView?.findViewById(R.id.pass_password)
passKeyView = rootView?.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
@@ -162,7 +172,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
@@ -194,22 +204,22 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
super.onResume()
// To check checkboxes if a text is present
passwordView?.addTextChangedListener(passwordTextWatcher)
passKeyView?.addTextChangedListener(passwordTextWatcher)
}
override fun onPause() {
super.onPause()
passwordView?.removeTextChangedListener(passwordTextWatcher)
passKeyView?.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passwordView != null
&& passKeyView != null
&& passwordRepeatView != null) {
mMasterPassword = passwordView!!.text.toString()
mMasterPassword = passKeyView!!.passwordString
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match

View File

@@ -26,14 +26,14 @@ class IconPickerFragment : DatabaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.icon_picker_pager)
tabLayout = view.findViewById(R.id.icon_picker_tabs)
viewPager = view.findViewById(R.id.tabs_view_pager)
tabLayout = view.findViewById(R.id.tabs_layout)
resetAppTimeoutWhenViewFocusedOrChanged(view)
arguments?.apply {

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class KeyGeneratorFragment : DatabaseFragment() {
private var keyGeneratorPagerAdapter: KeyGeneratorPagerAdapter? = null
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
private var mSelectedTab = KeyGeneratorTab.PASSWORD
private var mOnPageChangeCallback: ViewPager2.OnPageChangeCallback = object:
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
mSelectedTab = KeyGeneratorTab.getKeyGeneratorTabByPosition(position)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
keyGeneratorPagerAdapter = KeyGeneratorPagerAdapter(this, )
viewPager = view.findViewById(R.id.tabs_view_pager)
tabLayout = view.findViewById(R.id.tabs_layout)
viewPager.adapter = keyGeneratorPagerAdapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
}.attach()
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
resetAppTimeoutWhenViewFocusedOrChanged(view)
arguments?.apply {
if (containsKey(PASSWORD_TAB_ARG)) {
viewPager.currentItem = getInt(PASSWORD_TAB_ARG)
}
remove(PASSWORD_TAB_ARG)
}
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
when (mSelectedTab) {
KeyGeneratorTab.PASSWORD -> {
mKeyGeneratorViewModel.requirePasswordGeneration()
}
KeyGeneratorTab.PASSPHRASE -> {
mKeyGeneratorViewModel.requirePassphraseGeneration()
}
}
}
mKeyGeneratorViewModel.keyGeneratedValidated.observe(viewLifecycleOwner) {
when (mSelectedTab) {
KeyGeneratorTab.PASSWORD -> {
mKeyGeneratorViewModel.validatePasswordGenerated()
}
KeyGeneratorTab.PASSPHRASE -> {
mKeyGeneratorViewModel.validatePassphraseGenerated()
}
}
}
}
override fun onDestroyView() {
viewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback)
super.onDestroyView()
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
enum class KeyGeneratorTab(@StringRes val stringId: Int) {
PASSWORD(R.string.password), PASSPHRASE(R.string.passphrase);
companion object {
fun getKeyGeneratorTabByPosition(position: Int): KeyGeneratorTab {
return when (position) {
0 -> PASSWORD
else -> PASSPHRASE
}
}
}
}
companion object {
private const val PASSWORD_TAB_ARG = "PASSWORD_TAB_ARG"
fun getInstance(keyGeneratorTab: KeyGeneratorTab): KeyGeneratorFragment {
val fragment = KeyGeneratorFragment()
fragment.arguments = Bundle().apply {
putInt(PASSWORD_TAB_ARG, keyGeneratorTab.ordinal)
}
return fragment
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PassphraseGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var sliderWordCount: Slider
private lateinit var wordCountText: EditText
private lateinit var charactersCountText: TextView
private lateinit var wordSeparator: EditText
private lateinit var wordCaseSpinner: Spinner
private var minSliderWordCount: Int = 0
private var maxSliderWordCount: Int = 0
private var wordCaseAdapter: ArrayAdapter<String>? = null
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_generate_passphrase, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.passphrase_view)
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
sliderWordCount = view.findViewById(R.id.slider_word_count)
wordCountText = view.findViewById(R.id.word_count)
charactersCountText = view.findViewById(R.id.character_count)
wordSeparator = view.findViewById(R.id.word_separator)
wordCaseSpinner = view.findViewById(R.id.word_case)
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
contextThemed?.let { context ->
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(context)
passphraseCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
wordCaseAdapter = ArrayAdapter(context,
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.word_case_array)).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
wordCaseSpinner.adapter = wordCaseAdapter
}
loadSettings()
var listenSlider = true
var listenEditText = true
sliderWordCount.addOnChangeListener { _, value, _ ->
try {
listenEditText = false
if (listenSlider) {
wordCountText.setText(value.toInt().toString())
}
} catch (e: Exception) {
Log.e(TAG, "Unable to set the word count value", e)
} finally {
listenEditText = true
}
}
sliderWordCount.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
// TODO upgrade material-components lib
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
@SuppressLint("RestrictedApi")
override fun onStartTrackingTouch(slider: Slider) {}
@SuppressLint("RestrictedApi")
override fun onStopTrackingTouch(slider: Slider) {
generatePassphrase()
}
})
wordCountText.doOnTextChanged { _, _, _, _ ->
if (listenEditText) {
try {
listenSlider = false
setSliderValue(getWordCount())
} catch (e: Exception) {
Log.e(TAG, "Unable to get the word count value", e)
} finally {
listenSlider = true
generatePassphrase()
}
}
}
wordSeparator.doOnTextChanged { _, _, _, _ ->
generatePassphrase()
}
wordCaseSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
generatePassphrase()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
generatePassphrase()
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
}
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
generatePassphrase()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
private fun getWordCount(): Int {
return try {
Integer.valueOf(wordCountText.text.toString())
} catch (numberException: NumberFormatException) {
minSliderWordCount
}
}
private fun setWordCount(wordCount: Int) {
setSliderValue(wordCount)
wordCountText.setText(wordCount.toString())
}
private fun setSliderValue(value: Int) {
when {
value < minSliderWordCount -> {
sliderWordCount.value = minSliderWordCount.toFloat()
}
value > maxSliderWordCount -> {
sliderWordCount.value = maxSliderWordCount.toFloat()
}
else -> {
sliderWordCount.value = value.toFloat()
}
}
}
private fun getWordSeparator(): String {
return wordSeparator.text.toString().ifEmpty { " " }
}
private fun getWordCase(): PassphraseGenerator.WordCase {
var wordCase = PassphraseGenerator.WordCase.LOWER_CASE
try {
wordCase = PassphraseGenerator.WordCase.getByOrdinal(wordCaseSpinner.selectedItemPosition)
} catch (caseException: Exception) {
Log.e(TAG, "Unable to retrieve the word case", caseException)
}
return wordCase
}
private fun setWordCase(wordCase: PassphraseGenerator.WordCase) {
wordCaseSpinner.setSelection(wordCase.ordinal)
}
private fun getSeparator(): String {
return wordSeparator.text?.toString() ?: ""
}
private fun setSeparator(separator: String) {
wordSeparator.setText(separator)
}
private fun generatePassphrase() {
var passphrase = ""
try {
passphrase = PassphraseGenerator().generatePassphrase(
getWordCount(),
getWordSeparator(),
getWordCase())
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a passphrase", e)
}
passKeyView.passwordString = passphrase
charactersCountText.text = getString(R.string.character_count, passphrase.length)
}
override fun onDestroy() {
saveSettings()
super.onDestroy()
}
private fun saveSettings() {
context?.let { context ->
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
}
}
private fun loadSettings() {
context?.let { context ->
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
}
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
companion object {
private const val TAG = "PassphraseGnrtrFrgmt"
}
}

View File

@@ -0,0 +1,358 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PasswordGeneratorFragment : DatabaseFragment() {
private lateinit var passKeyView: PassKeyView
private lateinit var sliderLength: Slider
private lateinit var lengthEditView: EditText
private lateinit var uppercaseCompound: CompoundButton
private lateinit var lowercaseCompound: CompoundButton
private lateinit var digitsCompound: CompoundButton
private lateinit var minusCompound: CompoundButton
private lateinit var underlineCompound: CompoundButton
private lateinit var spaceCompound: CompoundButton
private lateinit var specialsCompound: CompoundButton
private lateinit var bracketsCompound: CompoundButton
private lateinit var extendedCompound: CompoundButton
private lateinit var considerCharsEditText: EditText
private lateinit var ignoreCharsEditText: EditText
private lateinit var atLeastOneCompound: CompoundButton
private lateinit var excludeAmbiguousCompound: CompoundButton
private var minLengthSlider: Int = 0
private var maxLengthSlider: Int = 0
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_generate_password, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
passKeyView = view.findViewById(R.id.password_view)
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
sliderLength = view.findViewById(R.id.slider_length)
lengthEditView = view.findViewById(R.id.length)
uppercaseCompound = view.findViewById(R.id.upperCase_filter)
lowercaseCompound = view.findViewById(R.id.lowerCase_filter)
digitsCompound = view.findViewById(R.id.digits_filter)
minusCompound = view.findViewById(R.id.minus_filter)
underlineCompound = view.findViewById(R.id.underline_filter)
spaceCompound = view.findViewById(R.id.space_filter)
specialsCompound = view.findViewById(R.id.special_filter)
bracketsCompound = view.findViewById(R.id.brackets_filter)
extendedCompound = view.findViewById(R.id.extendedASCII_filter)
considerCharsEditText = view.findViewById(R.id.consider_chars_filter)
ignoreCharsEditText = view.findViewById(R.id.ignore_chars_filter)
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
contextThemed?.let { context ->
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
View.VISIBLE else View.GONE
val clipboardHelper = ClipboardHelper(context)
passwordCopyView?.setOnClickListener {
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
}
minLengthSlider = resources.getInteger(R.integer.password_generator_length_min)
maxLengthSlider = resources.getInteger(R.integer.password_generator_length_max)
loadSettings()
uppercaseCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
lowercaseCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
digitsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
minusCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
underlineCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
spaceCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
specialsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
bracketsCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
extendedCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
considerCharsEditText.doOnTextChanged { _, _, _, _ ->
generatePassword()
}
ignoreCharsEditText.doOnTextChanged { _, _, _, _ ->
generatePassword()
}
atLeastOneCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
excludeAmbiguousCompound.setOnCheckedChangeListener { _, _ ->
generatePassword()
}
var listenSlider = true
var listenEditText = true
sliderLength.addOnChangeListener { _, value, _ ->
try {
listenEditText = false
if (listenSlider) {
lengthEditView.setText(value.toInt().toString())
}
} catch (e: Exception) {
Log.e(TAG, "Unable to set the length value", e)
} finally {
listenEditText = true
}
}
sliderLength.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
// TODO upgrade material-components lib
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
@SuppressLint("RestrictedApi")
override fun onStartTrackingTouch(slider: Slider) {}
@SuppressLint("RestrictedApi")
override fun onStopTrackingTouch(slider: Slider) {
generatePassword()
}
})
lengthEditView.doOnTextChanged { _, _, _, _ ->
if (listenEditText) {
try {
listenSlider = false
setSliderValue(getPasswordLength())
} catch (e: Exception) {
Log.e(TAG, "Unable to get the length value", e)
} finally {
listenSlider = true
generatePassword()
}
}
}
// Pre-populate a password to possibly save the user a few clicks
generatePassword()
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
}
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
generatePassword()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
private fun getPasswordLength(): Int {
return try {
Integer.valueOf(lengthEditView.text.toString())
} catch (numberException: NumberFormatException) {
minLengthSlider
}
}
private fun setPasswordLength(passwordLength: Int) {
setSliderValue(passwordLength)
lengthEditView.setText(passwordLength.toString())
}
private fun getOptions(): Set<String> {
val optionsSet = mutableSetOf<String>()
if (uppercaseCompound.isChecked)
optionsSet.add(getString(R.string.value_password_uppercase))
if (lowercaseCompound.isChecked)
optionsSet.add(getString(R.string.value_password_lowercase))
if (digitsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_digits))
if (minusCompound.isChecked)
optionsSet.add(getString(R.string.value_password_minus))
if (underlineCompound.isChecked)
optionsSet.add(getString(R.string.value_password_underline))
if (spaceCompound.isChecked)
optionsSet.add(getString(R.string.value_password_space))
if (specialsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_special))
if (bracketsCompound.isChecked)
optionsSet.add(getString(R.string.value_password_brackets))
if (extendedCompound.isChecked)
optionsSet.add(getString(R.string.value_password_extended))
if (atLeastOneCompound.isChecked)
optionsSet.add(getString(R.string.value_password_atLeastOne))
if (excludeAmbiguousCompound.isChecked)
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
return optionsSet
}
private fun setOptions(options: Set<String>) {
uppercaseCompound.isChecked = false
lowercaseCompound.isChecked = false
digitsCompound.isChecked = false
minusCompound.isChecked = false
underlineCompound.isChecked = false
spaceCompound.isChecked = false
specialsCompound.isChecked = false
bracketsCompound.isChecked = false
extendedCompound.isChecked = false
atLeastOneCompound.isChecked = false
excludeAmbiguousCompound.isChecked = false
for (option in options) {
when (option) {
getString(R.string.value_password_uppercase) -> uppercaseCompound.isChecked = true
getString(R.string.value_password_lowercase) -> lowercaseCompound.isChecked = true
getString(R.string.value_password_digits) -> digitsCompound.isChecked = true
getString(R.string.value_password_minus) -> minusCompound.isChecked = true
getString(R.string.value_password_underline) -> underlineCompound.isChecked = true
getString(R.string.value_password_space) -> spaceCompound.isChecked = true
getString(R.string.value_password_special) -> specialsCompound.isChecked = true
getString(R.string.value_password_brackets) -> bracketsCompound.isChecked = true
getString(R.string.value_password_extended) -> extendedCompound.isChecked = true
getString(R.string.value_password_atLeastOne) -> atLeastOneCompound.isChecked = true
getString(R.string.value_password_excludeAmbiguous) -> excludeAmbiguousCompound.isChecked = true
}
}
}
private fun getConsiderChars(): String {
return considerCharsEditText.text.toString()
}
private fun setConsiderChars(chars: String) {
considerCharsEditText.setText(chars)
}
private fun getIgnoreChars(): String {
return ignoreCharsEditText.text.toString()
}
private fun setIgnoreChars(chars: String) {
ignoreCharsEditText.setText(chars)
}
private fun generatePassword() {
var password = ""
try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
uppercaseCompound.isChecked,
lowercaseCompound.isChecked,
digitsCompound.isChecked,
minusCompound.isChecked,
underlineCompound.isChecked,
spaceCompound.isChecked,
specialsCompound.isChecked,
bracketsCompound.isChecked,
extendedCompound.isChecked,
getConsiderChars(),
getIgnoreChars(),
atLeastOneCompound.isChecked,
excludeAmbiguousCompound.isChecked)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
passKeyView.passwordString = password
}
override fun onDestroy() {
saveSettings()
super.onDestroy()
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
private fun saveSettings() {
context?.let { context ->
PreferencesUtil.setDefaultPasswordOptions(context, getOptions())
PreferencesUtil.setDefaultPasswordLength(context, getPasswordLength())
PreferencesUtil.setDefaultPasswordConsiderChars(context, getConsiderChars())
PreferencesUtil.setDefaultPasswordIgnoreChars(context, getIgnoreChars())
}
}
private fun loadSettings() {
context?.let { context ->
setOptions(PreferencesUtil.getDefaultPasswordOptions(context))
setPasswordLength(PreferencesUtil.getDefaultPasswordLength(context))
setConsiderChars(PreferencesUtil.getDefaultPasswordConsiderChars(context))
setIgnoreChars(PreferencesUtil.getDefaultPasswordIgnoreChars(context))
}
}
private fun setSliderValue(value: Int) {
when {
value < minLengthSlider -> {
sliderLength.value = minLengthSlider.toFloat()
}
value > maxLengthSlider -> {
sliderLength.value = maxLengthSlider.toFloat()
}
else -> {
sliderLength.value = value.toFloat()
}
}
}
companion object {
private const val TAG = "PasswordGeneratorFrgmt"
}
}

View File

@@ -0,0 +1,25 @@
package com.kunzisoft.keepass.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.kunzisoft.keepass.activities.fragments.PassphraseGeneratorFragment
import com.kunzisoft.keepass.activities.fragments.PasswordGeneratorFragment
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
class KeyGeneratorPagerAdapter(fragment: Fragment)
: FragmentStateAdapter(fragment) {
private val passwordGeneratorFragment = PasswordGeneratorFragment()
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
override fun getItemCount(): Int {
return KeyGeneratorFragment.KeyGeneratorTab.values().size
}
override fun createFragment(position: Int): Fragment {
return when (KeyGeneratorFragment.KeyGeneratorTab.getKeyGeneratorTabByPosition(position)) {
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD -> passwordGeneratorFragment
KeyGeneratorFragment.KeyGeneratorTab.PASSPHRASE -> passphraseGeneratorFragment
}
}
}

View File

@@ -26,7 +26,7 @@ import kotlinx.coroutines.*
*/
class IOActionTask<T>(
private val action: () -> T ,
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
private val afterActionListener: ((T?) -> Unit)? = null) {
private val mainScope = CoroutineScope(Dispatchers.Main)
@@ -42,7 +42,7 @@ class IOActionTask<T>(
}
}
withContext(Dispatchers.Main) {
afterActionDatabaseListener?.invoke(asyncResult.await())
afterActionListener?.invoke(asyncResult.await())
}
}
}

View File

@@ -60,6 +60,11 @@ object TemplateField {
const val LABEL_SECURE_NOTE = "Secure Note"
const val LABEL_MEMBERSHIP = "Membership"
fun isStandardPasswordName(context: Context, name: String): Boolean {
return name.equals(LABEL_PASSWORD, true)
|| name == getLocalizedName(context, LABEL_PASSWORD)
}
fun isStandardFieldName(name: String): Boolean {
return arrayOf(
LABEL_TITLE,

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.password
import me.gosimple.nbvcxz.resources.Generator
class PassphraseGenerator {
@Throws(IllegalArgumentException::class)
fun generatePassphrase(wordCount: Int,
wordSeparator: String,
wordCase: WordCase): String {
// From eff_large dictionary
return when (wordCase) {
WordCase.LOWER_CASE -> {
Generator.generatePassphrase(wordSeparator, wordCount)
}
WordCase.UPPER_CASE -> {
applyWordCase(wordCount, wordSeparator) { word ->
word.uppercase()
}
}
WordCase.TITLE_CASE -> {
applyWordCase(wordCount, wordSeparator) { word ->
word.replaceFirstChar { char -> char.uppercaseChar() }
}
}
}
}
private fun applyWordCase(wordCount: Int,
wordSeparator: String,
wordAction: (word: String) -> String): String {
val splitWords = Generator.generatePassphrase(TEMP_SPLIT, wordCount).split(TEMP_SPLIT)
val stringBuilder = StringBuilder()
splitWords.forEach {
stringBuilder
.append(wordAction(it))
.append(wordSeparator)
}
return stringBuilder.toString().removeSuffix(wordSeparator)
}
enum class WordCase {
LOWER_CASE,
UPPER_CASE,
TITLE_CASE;
companion object {
fun getByOrdinal(position: Int): WordCase {
return when (position) {
0 -> LOWER_CASE
1 -> UPPER_CASE
else -> TITLE_CASE
}
}
}
}
companion object {
private const val TEMP_SPLIT = "-"
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.password
import android.content.res.Resources
import android.graphics.Color
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.IOActionTask
import kotlinx.coroutines.*
import me.gosimple.nbvcxz.Nbvcxz
import me.gosimple.nbvcxz.resources.Configuration
import me.gosimple.nbvcxz.resources.ConfigurationBuilder
import java.util.*
import kotlin.math.min
class PasswordEntropy(actionOnInitFinished: (() -> Unit)? = null) {
private var mPasswordEntropyCalculator: Nbvcxz? = null
private var entropyJob: Job? = null
init {
IOActionTask({
// Create the password generator object
val configuration: Configuration = ConfigurationBuilder()
.setLocale(Locale.getDefault())
.setMinimumEntropy(80.0)
.createConfiguration()
mPasswordEntropyCalculator = Nbvcxz(configuration)
}, {
actionOnInitFinished?.invoke()
}).execute()
}
enum class Strength(val color: Int) {
RISKY(Color.rgb(224, 56, 56)),
VERY_GUESSABLE(Color.rgb(196, 63, 49)),
SOMEWHAT_GUESSABLE(Color.rgb(219, 152, 55)),
SAFELY_UNGUESSABLE(Color.rgb(118, 168, 24)),
VERY_UNGUESSABLE(Color.rgb(37, 152, 41))
}
data class EntropyStrength(val strength: Strength,
val entropy: Double,
val estimationPercent: Int) {
override fun toString(): String {
return "EntropyStrength(strength=$strength, entropy=$entropy, estimationPercent=$estimationPercent)"
}
}
fun getEntropyStrength(passwordString: String,
entropyStrengthResult: (EntropyStrength) -> Unit) {
entropyStrengthResult.invoke(EntropyStrength(Strength.RISKY, CALCULATE_ENTROPY, 0))
entropyJob?.cancel()
entropyJob = CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<EntropyStrength?> = async {
try {
if (passwordString.length <= MAX_PASSWORD_LENGTH) {
val estimate = mPasswordEntropyCalculator?.estimate(passwordString)
val basicScore = estimate?.basicScore ?: 0
val entropy = estimate?.entropy ?: 0.0
val percentScore = min(entropy * 100 / 200, 100.0).toInt()
val strength =
if (basicScore == 0 || percentScore < 10) {
Strength.RISKY
} else if (basicScore == 1 || percentScore < 20) {
Strength.VERY_GUESSABLE
} else if (basicScore == 2 || percentScore < 33) {
Strength.SOMEWHAT_GUESSABLE
} else if (basicScore == 3 || percentScore < 50) {
Strength.SAFELY_UNGUESSABLE
} else if (basicScore == 4) {
Strength.VERY_UNGUESSABLE
} else {
Strength.RISKY
}
EntropyStrength(strength, entropy, percentScore)
} else {
EntropyStrength(Strength.VERY_UNGUESSABLE, HIGH_ENTROPY, 100)
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
withContext(Dispatchers.Main) {
asyncResult.await()?.let { entropyStrength ->
entropyStrengthResult.invoke(entropyStrength)
}
}
}
}
}
companion object {
private const val MAX_PASSWORD_LENGTH = 128
private const val CALCULATE_ENTROPY = -1.0
private const val HIGH_ENTROPY = 1000.0
fun getStringEntropy(resources: Resources, entropy: Double): String {
return when (entropy) {
CALCULATE_ENTROPY -> {
resources.getString(R.string.entropy_calculate)
}
HIGH_ENTROPY -> {
resources.getString(R.string.entropy_high)
}
else -> {
resources.getString(
R.string.entropy,
"%.${2}f".format(entropy)
)
}
}
}
}
}

View File

@@ -20,33 +20,17 @@
package com.kunzisoft.keepass.password
import android.content.res.Resources
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import com.kunzisoft.keepass.R
import java.security.SecureRandom
import java.util.*
class PasswordGenerator(private val resources: Resources) {
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
private fun extendedChars(): String {
val charSet = StringBuilder()
// [U+0080, U+009F] are C1 control characters,
// U+00A0 is non-breaking space
run {
var ch = '\u00A1'
while (ch <= '\u00AC') {
charSet.append(ch)
++ch
}
}
// U+00AD is soft hyphen (format character)
var ch = '\u00AE'
while (ch < '\u00FF') {
charSet.append(ch)
++ch
}
charSet.append('\u00FF')
return charSet.toString()
}
@Throws(IllegalArgumentException::class)
fun generatePassword(length: Int,
upperCase: Boolean,
@@ -57,7 +41,11 @@ class PasswordGenerator(private val resources: Resources) {
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean): String {
extended: Boolean,
considerChars: String,
ignoreChars: String,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean): String {
// Desired password length is 0 or less
if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
@@ -72,74 +60,164 @@ class PasswordGenerator(private val resources: Resources) {
&& !space
&& !specials
&& !brackets
&& !extended) {
&& !extended
&& considerChars.isEmpty()) {
throw IllegalArgumentException(resources.getString(R.string.error_pass_gen_type))
}
val characterSet = getCharacterSet(
upperCase,
lowerCase,
digits,
minus,
underline,
space,
specials,
brackets,
extended)
val size = characterSet.length
val buffer = StringBuilder()
val random = SecureRandom() // use more secure variant of Random!
if (size > 0) {
for (i in 0 until length) {
buffer.append(characterSet[random.nextInt(size)])
// Filter builder
val passwordFilters = PasswordFilters().apply {
this.length = length
this.ignoreChars = ignoreChars
if (excludeAmbiguousChar)
this.ignoreChars += AMBIGUOUS_CHARS
if (upperCase) {
addFilter(
UPPERCASE_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (lowerCase) {
addFilter(
LOWERCASE_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (digits) {
addFilter(
DIGIT_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (minus) {
addFilter(
MINUS_CHAR,
if (atLeastOneFromEach) 1 else 0
)
}
if (underline) {
addFilter(
UNDERLINE_CHAR,
if (atLeastOneFromEach) 1 else 0
)
}
if (space) {
addFilter(
SPACE_CHAR,
if (atLeastOneFromEach) 1 else 0
)
}
if (specials) {
addFilter(
SPECIAL_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (brackets) {
addFilter(
BRACKET_CHARS,
if (atLeastOneFromEach) 1 else 0
)
}
if (extended) {
addFilter(
extendedChars(),
if (atLeastOneFromEach) 1 else 0
)
}
if (considerChars.isNotEmpty()) {
addFilter(
considerChars,
if (atLeastOneFromEach) 1 else 0
)
}
}
return buffer.toString()
return generateRandomString(SecureRandom(), passwordFilters)
}
private fun getCharacterSet(upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean): String {
val charSet = StringBuilder()
private fun generateRandomString(random: Random, passwordFilters: PasswordFilters): String {
val randomString = StringBuilder()
if (upperCase) {
charSet.append(UPPERCASE_CHARS)
// Allocate appropriate memory for the password.
var requiredCharactersLeft = passwordFilters.getRequiredCharactersLeft()
// Build the password.
for (i in 0 until passwordFilters.length) {
var selectableChars: String = if (requiredCharactersLeft < passwordFilters.length - i) {
// choose from any group at random
passwordFilters.getSelectableChars()
} else {
// choose only from a group that we need to satisfy a minimum for.
passwordFilters.getSelectableCharsForNeed()
}
passwordFilters.ignoreChars.forEach {
selectableChars = selectableChars.replace(it.toString(), "")
}
// Now that the string is built, get the next random character.
val selectableCharsMaxIndex = selectableChars.length
val randomSelectableCharsIndex = if (selectableCharsMaxIndex > 0) random.nextInt(selectableCharsMaxIndex) else 0
val nextChar = selectableChars[randomSelectableCharsIndex]
// Put at random position
val randomStringMaxIndex = randomString.length
val randomStringIndex = if (randomStringMaxIndex > 0) random.nextInt(randomStringMaxIndex) else 0
randomString.insert(randomStringIndex, nextChar)
// Now figure out where it came from, and decrement the appropriate minimum value
passwordFilters.getFilterThatContainsChar(nextChar)?.let {
if (it.minCharsNeeded > 0) {
it.minCharsNeeded--
requiredCharactersLeft--
}
}
}
if (lowerCase) {
charSet.append(LOWERCASE_CHARS)
}
if (digits) {
charSet.append(DIGIT_CHARS)
}
if (minus) {
charSet.append(MINUS_CHAR)
}
if (underline) {
charSet.append(UNDERLINE_CHAR)
}
if (space) {
charSet.append(SPACE_CHAR)
}
if (specials) {
charSet.append(SPECIAL_CHARS)
}
if (brackets) {
charSet.append(BRACKET_CHARS)
}
if (extended) {
charSet.append(extendedChars())
return randomString.toString()
}
private data class Filter(var chars: String,
var minCharsNeeded: Int)
private class PasswordFilters {
var length: Int = 0
var ignoreChars = ""
val filters = mutableListOf<Filter>()
fun addFilter(chars: String, minCharsNeeded: Int) {
filters.add(Filter(chars, minCharsNeeded))
}
return charSet.toString()
fun getRequiredCharactersLeft(): Int {
var charsRequired = 0
filters.forEach {
charsRequired += it.minCharsNeeded
}
return charsRequired
}
fun getSelectableChars(): String {
val stringBuilder = StringBuilder()
filters.forEach {
stringBuilder.append(it.chars)
}
return stringBuilder.toString()
}
fun getFilterThatContainsChar(char: Char): Filter? {
return filters.find { it.chars.contains(char) }
}
fun getSelectableCharsForNeed(): String {
val selectableChars = StringBuilder()
// choose only from a group that we need to satisfy a minimum for.
filters.forEach {
if (it.minCharsNeeded > 0) {
selectableChars.append(it.chars)
}
}
return selectableChars.toString()
}
}
companion object {
@@ -151,5 +229,75 @@ class PasswordGenerator(private val resources: Resources) {
private const val SPACE_CHAR = " "
private const val SPECIAL_CHARS = "!\"#$%&'*+,./:;=?@\\^`"
private const val BRACKET_CHARS = "[]{}()<>"
private const val AMBIGUOUS_CHARS = "iI|lLoO01"
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
private fun extendedChars(): String {
val charSet = StringBuilder()
// [U+0080, U+009F] are C1 control characters,
// U+00A0 is non-breaking space
run {
var ch = '\u00A1'
while (ch <= '\u00AC') {
charSet.append(ch)
++ch
}
}
// U+00AD is soft hyphen (format character)
var ch = '\u00AE'
while (ch < '\u00FF') {
charSet.append(ch)
++ch
}
charSet.append('\u00FF')
return charSet.toString()
}
fun getColorizedPassword(password: String): Spannable {
val spannableString = SpannableStringBuilder()
if (password.isNotEmpty()) {
password.forEach {
when {
UPPERCASE_CHARS.contains(it)||
LOWERCASE_CHARS.contains(it) -> {
spannableString.append(it)
}
DIGIT_CHARS.contains(it) -> {
// RED
spannableString.append(colorizeChar(it, Color.rgb(246, 79, 62)))
}
SPECIAL_CHARS.contains(it) -> {
// Blue
spannableString.append(colorizeChar(it, Color.rgb(39, 166, 228)))
}
MINUS_CHAR.contains(it)||
UNDERLINE_CHAR.contains(it)||
BRACKET_CHARS.contains(it) -> {
// Purple
spannableString.append(colorizeChar(it, Color.rgb(185, 38, 209)))
}
extendedChars().contains(it) -> {
// Green
spannableString.append(colorizeChar(it, Color.rgb(44, 181, 50)))
}
else -> {
spannableString.append(it)
}
}
}
}
return spannableString
}
private fun colorizeChar(char: Char, color: Int): Spannable {
val spannableColorChar = SpannableString(char.toString())
spannableColorChar.setSpan(
ForegroundColorSpan(color),
0,
1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
return spannableColorChar
}
}
}

View File

@@ -32,6 +32,7 @@ import com.kunzisoft.keepass.activities.stylish.Stylish
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.education.Education
import com.kunzisoft.keepass.password.PassphraseGenerator
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import java.util.*
@@ -135,6 +136,18 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.show_uuid_default))
}
fun hideProtectedValue(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_password_key),
context.resources.getBoolean(R.bool.hide_password_default))
}
fun colorizePassword(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.colorize_password_key),
context.resources.getBoolean(R.bool.colorize_password_default))
}
fun showExpiredEntries(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return ! prefs.getBoolean(context.getString(R.string.hide_expired_entries_key),
@@ -192,15 +205,118 @@ object PreferencesUtil {
fun getDefaultPasswordLength(context: Context): Int {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getInt(context.getString(R.string.password_length_key),
Integer.parseInt(context.getString(R.string.default_password_length)))
return prefs.getInt(context.getString(R.string.password_generator_length_key),
context.resources.getInteger(R.integer.password_generator_length_default))
}
fun getDefaultPasswordCharacters(context: Context): Set<String>? {
fun setDefaultPasswordLength(context: Context, passwordLength: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.password_generator_length_key),
passwordLength
)
apply()
}
}
fun getDefaultPasswordOptions(context: Context): Set<String> {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getStringSet(context.getString(R.string.list_password_generator_options_key),
HashSet(listOf(*context.resources
.getStringArray(R.array.list_password_generator_options_default_values))))
return prefs.getStringSet(context.getString(R.string.password_generator_options_key),
HashSet(listOf(*context.resources
.getStringArray(R.array.list_password_generator_options_default_values)))) ?: setOf()
}
fun setDefaultPasswordOptions(context: Context, passwordOptionsSet: Set<String>) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putStringSet(
context.getString(R.string.password_generator_options_key),
passwordOptionsSet
)
apply()
}
}
fun getDefaultPasswordConsiderChars(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.password_generator_consider_chars_key),
context.getString(R.string.password_generator_consider_chars_default)) ?: ""
}
fun setDefaultPasswordConsiderChars(context: Context, considerChars: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.password_generator_consider_chars_key),
considerChars
)
apply()
}
}
fun getDefaultPasswordIgnoreChars(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.password_generator_ignore_chars_key),
context.getString(R.string.password_generator_ignore_chars_default)) ?: ""
}
fun setDefaultPasswordIgnoreChars(context: Context, ignoreChars: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.password_generator_ignore_chars_key),
ignoreChars
)
apply()
}
}
fun getDefaultPassphraseWordCount(context: Context): Int {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getInt(context.getString(R.string.passphrase_generator_word_count_key),
context.resources.getInteger(R.integer.passphrase_generator_word_count_default))
}
fun setDefaultPassphraseWordCount(context: Context, passphraseWordCount: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.passphrase_generator_word_count_key),
passphraseWordCount
)
apply()
}
}
fun getDefaultPassphraseWordCase(context: Context): PassphraseGenerator.WordCase {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return PassphraseGenerator.WordCase
.getByOrdinal(prefs.getInt(context
.getString(R.string.passphrase_generator_word_case_key),
0)
)
}
fun setDefaultPassphraseWordCase(context: Context, wordCase: PassphraseGenerator.WordCase) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.passphrase_generator_word_case_key),
wordCase.ordinal
)
apply()
}
}
fun getDefaultPassphraseSeparator(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.passphrase_generator_separator_key),
context.getString(R.string.passphrase_generator_separator_default)) ?: ""
}
fun setDefaultPassphraseSeparator(context: Context, separator: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.passphrase_generator_separator_key),
separator
)
apply()
}
}
fun isClipboardNotificationsEnable(context: Context): Boolean {
@@ -351,12 +467,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.sort_recycle_bin_bottom_default))
}
fun hideProtectedValue(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.hide_password_key),
context.resources.getBoolean(R.bool.hide_password_default))
}
fun fieldFontIsInVisibility(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.monospace_font_fields_enable_key),
@@ -609,9 +719,8 @@ object PreferencesUtil {
context.getString(R.string.lock_database_screen_off_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.lock_database_back_root_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.lock_database_show_button_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.password_length_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.list_password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.password_generator_length_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.password_generator_options_key) -> editor.putStringSet(name, getStringSetFromProperties(value))
context.getString(R.string.allow_copy_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.remember_database_locations_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_recent_files_key) -> editor.putBoolean(name, value.toBoolean())
@@ -659,6 +768,8 @@ object PreferencesUtil {
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.list_size_key) -> editor.putString(name, value)
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -46,7 +46,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var passwordView: EditText
private var passwordTextView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton
private var checkboxKeyFileView: CompoundButton
@@ -60,7 +60,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this)
passwordView = findViewById(R.id.password)
passwordTextView = findViewById(R.id.password_text_view)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
@@ -75,8 +75,8 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
}
passwordView.setOnEditorActionListener(onEditorActionListener)
passwordView.addTextChangedListener(object : TextWatcher {
passwordTextView.setOnEditorActionListener(onEditorActionListener)
passwordTextView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@@ -86,7 +86,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
checkboxPasswordView.isChecked = true
}
})
passwordView.setOnKeyListener { _, _, keyEvent ->
passwordTextView.setOnKeyListener { _, _, keyEvent ->
var handled = false
if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
@@ -108,11 +108,11 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
fun populatePasswordTextView(text: String?) {
if (text == null || text.isEmpty()) {
passwordView.setText("")
passwordTextView.setText("")
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = false
} else {
passwordView.setText(text)
passwordTextView.setText(text)
if (checkboxPasswordView.isChecked)
checkboxPasswordView.isChecked = true
}
@@ -137,7 +137,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
fun getMainCredential(): MainCredential {
return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked)
passwordView.text?.toString() else null
passwordTextView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null
}
@@ -163,7 +163,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
*/
fun retrieveCredentialForStorage(listener: CredentialStorageListener): ByteArray? {
return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordView.text?.toString())
CredentialStorage.PASSWORD -> listener.passwordToStore(passwordTextView.text?.toString())
CredentialStorage.KEY_FILE -> listener.keyfileToStore(keyFileSelectionView.uri)
CredentialStorage.HARDWARE_KEY -> listener.hardwareKeyToStore()
}
@@ -176,15 +176,15 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
}
fun requestPasswordFocus() {
passwordView.requestFocusFromTouch()
passwordTextView.requestFocusFromTouch()
}
// Auto select the password field and open keyboard
fun focusPasswordFieldAndOpenKeyboard() {
passwordView.postDelayed({
passwordView.requestFocusFromTouch()
passwordTextView.postDelayed({
passwordTextView.requestFocusFromTouch()
val inputMethodManager = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as? InputMethodManager?
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
inputMethodManager?.showSoftInput(passwordTextView, InputMethodManager.SHOW_IMPLICIT)
}, 100)
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.text.Editable
import android.text.InputType
import android.text.SpannableString
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.TextView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.settings.PreferencesUtil
class PassKeyView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy? = null
private val passwordInputLayout: TextInputLayout
private val passwordText: TextView
private val passwordStrengthProgress: LinearProgressIndicator
private val passwordEntropy: TextView
private var mViewHint: String = ""
private var mMaxLines: Int = 3
private var mShowPassword: Boolean = false
private var mPasswordTextWatcher: MutableList<TextWatcher> = mutableListOf()
private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatcher.forEach {
it.beforeTextChanged(charSequence, i, i1, i2)
}
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
mPasswordTextWatcher.forEach {
it.onTextChanged(charSequence, i, i1, i2)
}
}
override fun afterTextChanged(editable: Editable) {
mPasswordTextWatcher.forEach {
it.afterTextChanged(editable)
}
getEntropyStrength(editable.toString())
}
}
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.PassKeyView,
0, 0).apply {
try {
mViewHint = getString(R.styleable.PassKeyView_passKeyHint)
?: context.getString(R.string.password)
mMaxLines = getInteger(R.styleable.PassKeyView_passKeyMaxLines, mMaxLines)
mShowPassword = getBoolean(R.styleable.PassKeyView_passKeyVisible,
!PreferencesUtil.hideProtectedValue(context))
} finally {
recycle()
}
}
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_passkey, this)
passwordInputLayout = findViewById(R.id.password_input_layout)
passwordInputLayout?.hint = mViewHint
passwordText = findViewById(R.id.password_text)
if (mShowPassword) {
passwordText?.inputType = passwordText.inputType and
InputType.TYPE_TEXT_VARIATION_PASSWORD or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
}
passwordText?.maxLines = mMaxLines
passwordText?.applyFontVisibility()
passwordText.addTextChangedListener(passwordTextWatcher)
passwordStrengthProgress = findViewById(R.id.password_strength_progress)
passwordStrengthProgress?.apply {
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
progress = 0
max = 100
}
passwordEntropy = findViewById(R.id.password_entropy)
mPasswordEntropyCalculator = PasswordEntropy {
passwordText?.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
}
private fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator?.getEntropyStrength(passwordText) { entropyStrength ->
passwordStrengthProgress.apply {
post {
setIndicatorColor(entropyStrength.strength.color)
setProgressCompat(entropyStrength.estimationPercent, true)
}
}
passwordEntropy.apply {
post {
text = PasswordEntropy.getStringEntropy(resources, entropyStrength.entropy)
}
}
}
}
fun addTextChangedListener(textWatcher: TextWatcher) {
mPasswordTextWatcher.add(textWatcher)
}
fun removeTextChangedListener(textWatcher: TextWatcher) {
mPasswordTextWatcher.remove(textWatcher)
}
var passwordString: String
get() {
return passwordText.text.toString()
}
set(value) {
val spannableString =
if (PreferencesUtil.colorizePassword(context))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
passwordText.text = spannableString
}
}

View File

@@ -162,7 +162,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
if (templateAttribute.options.isAssociatedWithPasswordGenerator()) {
setOnActionClickListener({
mOnPasswordGenerationActionClickListener?.invoke(field)
}, R.drawable.ic_generate_password_white_24dp)
}, R.drawable.ic_random_white_24dp)
}
}
}

View File

@@ -1,9 +1,13 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import android.text.InputFilter
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
@@ -19,6 +23,9 @@ import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class TextEditFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@@ -123,7 +130,13 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
return valueView.text?.toString() ?: ""
}
set(value) {
valueView.setText(value)
val spannableString =
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.setText(spannableString)
}
override var default: String = ""
@@ -164,7 +177,11 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
fun setProtection(protection: Boolean) {
if (protection) {
labelView.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
valueView.inputType = valueView.inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
val visibilityTag = if (PreferencesUtil.hideProtectedValue(context))
InputType.TYPE_TEXT_VARIATION_PASSWORD
else
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
valueView.inputType = valueView.inputType or visibilityTag
}
}

View File

@@ -20,13 +20,18 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import android.text.InputFilter
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.text.util.Linkify
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.view.View.OnClickListener
import android.widget.RelativeLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
@@ -36,7 +41,10 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil
@@ -191,7 +199,13 @@ class TextFieldView @JvmOverloads constructor(context: Context,
return valueView.text.toString()
}
set(value) {
valueView.text = value
val spannableString =
if (PreferencesUtil.colorizePassword(context)
&& TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
valueView.text = spannableString
changeProtectedValueParameters()
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class KeyGeneratorViewModel: ViewModel() {
val keyGenerated : LiveData<String> get() = _keyGenerated
private val _keyGenerated = MutableLiveData<String>()
val keyGeneratedValidated : LiveData<Void?> get() = _keyGeneratedValidated
private val _keyGeneratedValidated = SingleLiveEvent<Void?>()
val requireKeyGeneration : LiveData<Void?> get() = _requireKeyGeneration
private val _requireKeyGeneration = SingleLiveEvent<Void?>()
val passwordGeneratedValidated : LiveData<Void?> get() = _passwordGeneratedValidated
private val _passwordGeneratedValidated = SingleLiveEvent<Void?>()
val requirePasswordGeneration : LiveData<Void?> get() = _requirePasswordGeneration
private val _requirePasswordGeneration = SingleLiveEvent<Void?>()
val passphraseGeneratedValidated : LiveData<Void?> get() = _passphraseGeneratedValidated
private val _passphraseGeneratedValidated = SingleLiveEvent<Void?>()
val requirePassphraseGeneration : LiveData<Void?> get() = _requirePassphraseGeneration
private val _requirePassphraseGeneration = SingleLiveEvent<Void?>()
fun setKeyGenerated(passKey: String) {
_keyGenerated.value = passKey
}
fun validateKeyGenerated() {
_keyGeneratedValidated.call()
}
fun validatePasswordGenerated() {
_passwordGeneratedValidated.call()
}
fun validatePassphraseGenerated() {
_passphraseGeneratedValidated.call()
}
fun requireKeyGeneration() {
_requireKeyGeneration.call()
}
fun requirePasswordGeneration() {
_requirePasswordGeneration.call()
}
fun requirePassphraseGeneration() {
_requirePassphraseGeneration.call()
}
}

View File

@@ -19,11 +19,11 @@ class SingleLiveEvent<T> : MutableLiveData<T>() {
}
// Observe the internal MutableLiveData
super.observe(owner, { t ->
super.observe(owner) { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
}
@MainThread

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/key_generator_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/key_generator_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/toolbar">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/key_generator_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.kunzisoft.keepass.view.ToolbarAction
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:theme="?attr/toolbarActionAppearance"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/key_generator_validation"
style="@style/KeepassDXStyle.Fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/validate"
android:src="@drawable/ic_check_white_24dp"
app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
android:paddingBottom="@dimen/card_view_margin_vertical"
tools:targetApi="o">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="240dp">
<View
android:layout_width="match_parent"
android:layout_height="180dp"
android:background="?attr/colorPrimary" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin_horizontal"
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginLeft="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:layout_marginTop="@dimen/card_view_margin_vertical"
android:layout_marginBottom="@dimen/card_view_margin_vertical"
android:layout_gravity="bottom"
app:cardCornerRadius="4dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
<com.kunzisoft.keepass.view.PassKeyView
android:id="@+id/passphrase_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/passphrase_copy_button"
android:layout_toLeftOf="@+id/passphrase_copy_button"
app:passKeyHint="@string/passphrase"
app:passKeyMaxLines="7"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/passphrase_copy_button"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/passphrase_view"
android:layout_alignBottom="@+id/passphrase_view"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:contentDescription="@string/menu_copy"
android:src="@drawable/ic_content_copy_white_24dp" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
<ScrollView
android:id="@+id/ScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/card_view_margin_horizontal"
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginLeft="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:layout_marginTop="@dimen/card_view_margin_vertical"
android:layout_marginBottom="@dimen/card_view_margin_vertical"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.slider.Slider
android:id="@+id/slider_word_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/word_count"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/word_count"
android:layout_toLeftOf="@+id/word_count"
android:contentDescription="@string/content_description_password_length"
android:stepSize="1"
android:value="@integer/passphrase_generator_word_count_default"
android:valueFrom="@integer/passphrase_generator_word_count_min"
android:valueTo="@integer/passphrase_generator_word_count_max"
app:thumbColor="?attr/chipFilterBackgroundColor"
app:trackColorActive="?attr/chipFilterBackgroundColor"
app:trackColorInactive="?attr/chipFilterBackgroundColorDisabled" />
<EditText
android:id="@+id/word_count"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:inputType="number"
android:maxLength="2"
android:maxLines="1" />
</RelativeLayout>
<TextView
android:id="@+id/character_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Character count: 51" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Spinner
android:id="@+id/word_case"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="12dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/word_separator_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/word_case"
android:layout_toRightOf="@+id/word_case">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/word_separator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/word_separator" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>
</LinearLayout>
</FrameLayout>
</ScrollView>
</LinearLayout>

View File

@@ -23,319 +23,242 @@
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/default_margin"
android:layout_height="match_parent"
android:importantForAutofill="noExcludeDescendants"
android:paddingBottom="@dimen/card_view_margin_vertical"
tools:targetApi="o">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="180dp">
<View
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/password_copy_button"
android:importantForAccessibility="no"
android:importantForAutofill="no"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/hint_generated_password"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="textPassword|textMultiLine"
android:maxLines="3"
tools:ignore="TextFields" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/password_copy_button"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/menu_copy"
android:src="@drawable/ic_content_copy_white_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/generate_password_button"
android:layout_margin="@dimen/button_margin"
android:layout_height="120dp"
android:background="?attr/colorPrimary" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:drawableEnd="@drawable/ic_generate_password_white_24dp"
android:drawableRight="@drawable/ic_generate_password_white_24dp"
android:paddingLeft="24dp"
android:paddingStart="24dp"
android:paddingRight="24dp"
android:paddingEnd="24dp"
android:text="@string/generate_password" />
</LinearLayout>
android:layout_marginStart="@dimen/card_view_margin_horizontal"
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginLeft="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:layout_marginTop="@dimen/card_view_margin_vertical"
android:layout_marginBottom="@dimen/card_view_margin_vertical"
android:layout_gravity="bottom"
app:cardCornerRadius="4dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
<com.kunzisoft.keepass.view.PassKeyView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/password_copy_button"
android:layout_toLeftOf="@+id/password_copy_button" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/password_copy_button"
style="@style/KeepassDXStyle.ImageButton.Simple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/password_view"
android:layout_alignBottom="@+id/password_view"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:contentDescription="@string/menu_copy"
android:src="@drawable/ic_content_copy_white_24dp" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
<ScrollView
android:id="@+id/ScrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/length_label"
android:text="@string/length"
android:layout_height="match_parent"
android:layout_width="match_parent" />
<EditText
android:id="@+id/length"
android:layout_width="50dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_height="wrap_content"
android:layout_below="@+id/length_label"
android:maxLines="1"
android:maxLength="3"
android:inputType="number"
android:text="@string/default_password_length"
android:hint="@string/hint_length"/>
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/seekbar_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/length"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignTop="@+id/length"
android:layout_toEndOf="@+id/length"
android:layout_toRightOf="@+id/length"
android:contentDescription="@string/content_description_password_length"
app:min="@string/min_password_length"
android:progress="@string/default_password_length"
android:max="@string/max_password_length"/>
</RelativeLayout>
android:layout_marginStart="@dimen/card_view_margin_horizontal"
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
android:layout_marginLeft="@dimen/card_view_margin_horizontal"
android:layout_marginRight="@dimen/card_view_margin_horizontal"
android:layout_marginTop="@dimen/card_view_margin_vertical"
android:layout_marginBottom="@dimen/card_view_margin_vertical"
app:cardCornerRadius="4dp">
<LinearLayout
android:id="@+id/RelativeLayout"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp">
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_uppercase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/uppercase"
android:checked="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_uppercase"
android:layout_toRightOf="@+id/cb_uppercase"
android:text="@string/visual_uppercase" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_lowercase"
android:layout_width="wrap_content"
<com.google.android.material.slider.Slider
android:id="@+id/slider_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lowercase"
android:checked="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_lowercase"
android:layout_toRightOf="@+id/cb_lowercase"
android:text="@string/visual_lowercase" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_digits"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/digits"
android:checked="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_digits"
android:layout_toRightOf="@+id/cb_digits"
android:text="@string/visual_digits" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/minus" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_minus"
android:layout_toRightOf="@+id/cb_minus"
android:text="@string/visual_minus" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_underline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/underline" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_underline"
android:layout_toRightOf="@+id/cb_underline"
android:text="@string/visual_underline" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_space"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/space" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_space"
android:layout_toRightOf="@+id/cb_space"
android:text="@string/visual_space" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_specials"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/special" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_specials"
android:layout_toRightOf="@+id/cb_specials"
android:text="@string/visual_special" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_brackets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/brackets"
android:layout_alignBottom="@+id/length"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:layout_width="wrap_content"
android:layout_toStartOf="@+id/length"
android:layout_toLeftOf="@+id/length"
android:contentDescription="@string/content_description_password_length"
android:stepSize="1"
android:value="@integer/password_generator_length_default"
android:valueFrom="@integer/password_generator_length_min"
android:valueTo="@integer/password_generator_length_max"
app:thumbColor="?attr/chipFilterBackgroundColor"
app:trackColorActive="?attr/chipFilterBackgroundColor"
app:trackColorInactive="?attr/chipFilterBackgroundColorDisabled" />
<EditText
android:id="@+id/length"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_brackets"
android:layout_toRightOf="@+id/cb_brackets"
android:text="@string/visual_brackets" />
android:inputType="number"
android:maxLength="3"
android:maxLines="1" />
</RelativeLayout>
<RelativeLayout
<com.google.android.material.chip.ChipGroup
android:id="@+id/password_filters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingVertical="16dp"
android:paddingTop="@dimen/default_margin"
android:paddingBottom="@dimen/default_margin">
<com.google.android.material.chip.Chip
android:id="@+id/upperCase_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/visual_uppercase"/>
<com.google.android.material.chip.Chip
android:id="@+id/lowerCase_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/visual_lowercase"/>
<com.google.android.material.chip.Chip
android:id="@+id/digits_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/visual_digits"/>
<com.google.android.material.chip.Chip
android:id="@+id/minus_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/visual_minus"/>
<com.google.android.material.chip.Chip
android:id="@+id/underline_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/visual_underline"/>
<com.google.android.material.chip.Chip
android:id="@+id/space_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/space"/>
<com.google.android.material.chip.Chip
android:id="@+id/special_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/visual_special"/>
<com.google.android.material.chip.Chip
android:id="@+id/brackets_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/visual_brackets"/>
<com.google.android.material.chip.Chip
android:id="@+id/extendedASCII_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/visual_extended"/>
</com.google.android.material.chip.ChipGroup>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/cb_extended"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/consider_chars_filter_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ignore_chars_filter_layout"
android:layout_width="0dp"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/consider_chars_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/consider_chars_filter"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ignore_chars_filter_layout"
app:layout_constraintStart_toEndOf="@+id/consider_chars_filter_layout"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ignore_chars_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ignore_chars_filter"/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/password_advanced"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingVertical="16dp"
android:paddingTop="@dimen/default_margin"
android:paddingBottom="@dimen/default_margin">
<com.google.android.material.chip.Chip
android:id="@+id/atLeastOne_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/extended_ASCII"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:checked="true"
android:text="@string/at_least_one_char"/>
<com.google.android.material.chip.Chip
android:id="@+id/excludeAmbiguous_filter"
style="@style/KeepassDXStyle.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:layout_toEndOf="@+id/cb_extended"
android:layout_toRightOf="@+id/cb_extended"
android:text="@string/visual_extended" />
</RelativeLayout>
android:checked="true"
android:text="@string/exclude_ambiguous_chars"/>
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</ScrollView>
</LinearLayout>

View File

@@ -71,25 +71,11 @@
android:text="@string/password"/>
<!-- Password Input -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input_layout"
<com.kunzisoft.keepass.view.PassKeyView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:importantForAutofill="no"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pass_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="yes"
android:autofillHints="newPassword"
android:maxLines="1"
android:hint="@string/hint_pass"/>
</com.google.android.material.textfield.TextInputLayout>
app:passKeyVisible="false"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_repeat_input_layout"
android:layout_width="match_parent"
@@ -100,14 +86,14 @@
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pass_conf_password"
android:id="@+id/password_confirmation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:ems="10"
android:importantForAccessibility="no"
android:importantForAutofill="yes"
android:autofillHints="newPassword"
android:maxLines="1"
android:importantForAutofill="no"
android:inputType="textPassword|textMultiLine"
android:maxLines="3"
android:hint="@string/hint_conf_pass"/>
</com.google.android.material.textfield.TextInputLayout>

View File

@@ -23,15 +23,15 @@
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/icon_picker_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/icon_picker_pager"
android:id="@+id/tabs_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -36,19 +36,16 @@
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/password_checkbox"
android:layout_toEndOf="@+id/password_checkbox"
android:importantForAccessibility="no"
android:importantForAutofill="no"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:id="@+id/password_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:hint="@string/password"
android:inputType="textPassword"
android:importantForAccessibility="no"
android:importantForAutofill="yes"
android:focusable="true"
android:focusableInTouchMode="true"

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:importantForAccessibility="no"
android:importantForAutofill="no"
app:endIconMode="password_toggle"
app:endIconTint="?attr/colorAccent"
tools:ignore="UnusedAttribute">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/hint_pass"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="textPassword|textMultiLine"
android:maxLines="3"
tools:ignore="TextFields" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/password_strength_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:trackCornerRadius="8dp"
app:layout_constraintTop_toBottomOf="@+id/password_input_layout"/>
<TextView
android:id="@+id/password_entropy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Entropy: 72.50 bit"
android:textSize="11sp"
android:layout_margin="4dp"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/password_input_layout"
app:layout_constraintEnd_toEndOf="@+id/password_input_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_generate"
android:icon="@drawable/ic_random_white_24dp"
android:title="@string/generate_password"
android:orderInCategory="8"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -41,6 +41,12 @@
<attr name="explanations" format="string" />
</declare-styleable>
<declare-styleable name="PassKeyView">
<attr name="passKeyHint" format="string" />
<attr name="passKeyMaxLines" format="integer" />
<attr name="passKeyVisible" format="boolean" />
</declare-styleable>
<!-- Specific keyboard attributes -->
<declare-styleable name="KeyboardView">
<!-- Default KeyboardView style. -->

View File

@@ -81,10 +81,6 @@
<bool name="lock_database_back_root_default" translatable="false">false</bool>
<string name="lock_database_show_button_key" translatable="false">lock_database_show_button_key</string>
<bool name="lock_database_show_button_default" translatable="false">true</bool>
<string name="password_length_key" translatable="false">password_length_key</string>
<string name="list_password_generator_options_key" translatable="false">list_password_generator_options_key</string>
<string name="hide_password_key" translatable="false">hide_password_key</string>
<bool name="hide_password_default" translatable="false">true</bool>
<string name="allow_copy_password_key" translatable="false">allow_copy_password_key</string>
<bool name="allow_copy_password_default" translatable="false">false</bool>
<string name="remember_database_locations_key" translatable="false">remember_database_locations_key</string>
@@ -195,12 +191,34 @@
<string name="list_size_key" translatable="false">list_size_key</string>
<string name="monospace_font_fields_enable_key" translatable="false">monospace_font_extra_fields_enable_key</string>
<bool name="monospace_font_fields_enable_default" translatable="false">true</bool>
<string name="hide_password_key" translatable="false">hide_password_key</string>
<bool name="hide_password_default" translatable="false">true</bool>
<string name="colorize_password_key" translatable="false">colorize_password_key</string>
<bool name="colorize_password_default" translatable="false">true</bool>
<string name="hide_expired_entries_key" translatable="false">hide_expired_entries_key</string>
<bool name="hide_expired_entries_default" translatable="false">false</bool>
<string name="enable_education_screens_key" translatable="false">enable_education_screens_key</string>
<bool name="enable_education_screens_default" translatable="false">true</bool>
<string name="reset_education_screens_key" translatable="false">relaunch_education_screens_key</string>
<!-- Password Generator Settings -->
<string name="password_generator_length_key" translatable="false">password_generator_length_key</string>
<integer name="password_generator_length_min" translatable="false">1</integer>
<integer name="password_generator_length_default" translatable="false">14</integer>
<integer name="password_generator_length_max" translatable="false">64</integer>
<string name="password_generator_options_key" translatable="false">password_generator_options_key</string>
<string name="password_generator_consider_chars_key" translatable="false">password_generator_consider_chars_key</string>
<string name="password_generator_consider_chars_default" translatable="false" />
<string name="password_generator_ignore_chars_key" translatable="false">password_generator_ignore_chars_key</string>
<string name="password_generator_ignore_chars_default" translatable="false" />
<string name="passphrase_generator_word_count_key" translatable="false">passphrase_generator_word_count_key</string>
<integer name="passphrase_generator_word_count_min" translatable="false">1</integer>
<integer name="passphrase_generator_word_count_default" translatable="false">8</integer>
<integer name="passphrase_generator_word_count_max" translatable="false">20</integer>
<string name="passphrase_generator_word_case_key" translatable="false">passphrase_generator_word_case_key</string>
<string name="passphrase_generator_separator_key" translatable="false">passphrase_generator_separator_key</string>
<string name="passphrase_generator_separator_default" translatable="false" />
<!-- Database Settings -->
<string name="settings_database_key" translatable="false">settings_database_key</string>
<string name="settings_database_security_key" translatable="false">settings_database_security_key</string>
@@ -365,11 +383,6 @@
<!-- WARNING ! module icon-pack-material must be import in gradle -->
<string name="setting_icon_pack_choose_default" translatable="false">@string/material_resource_id</string>
<!-- Password generator -->
<string name="min_password_length" translatable="false">1</string>
<string name="default_password_length" translatable="false">14</string>
<string name="max_password_length" translatable="false">128</string>
<string name="value_password_uppercase" translatable="false">value_password_uppercase</string>
<string name="value_password_lowercase" translatable="false">value_password_lowercase</string>
<string name="value_password_digits" translatable="false">value_password_digits</string>
@@ -379,6 +392,8 @@
<string name="value_password_special" translatable="false">value_password_special</string>
<string name="value_password_brackets" translatable="false">value_password_brackets</string>
<string name="value_password_extended" translatable="false">value_password_extended</string>
<string name="value_password_atLeastOne" translatable="false">value_password_atLeastOne</string>
<string name="value_password_excludeAmbiguous" translatable="false">value_password_excludeAmbiguous</string>
<string name="visual_uppercase" translatable="false">A-Z</string>
<string name="visual_lowercase" translatable="false">a-z</string>
<string name="visual_digits" translatable="false">0-9</string>
@@ -392,6 +407,7 @@
<item translatable="false">@string/value_password_uppercase</item>
<item translatable="false">@string/value_password_lowercase</item>
<item translatable="false">@string/value_password_digits</item>
<item translatable="false">@string/value_password_special</item>
</string-array>
<string-array name="list_password_generator_options_values">
<item translatable="false">@string/value_password_uppercase</item>

View File

@@ -69,6 +69,7 @@
<string name="discard">Discard</string>
<string name="entry_password_generator">Password generator</string>
<string name="content_description_password_length">Password length</string>
<string name="content_description_passphrase_word_count">Passphrase word count</string>
<string name="entry_add_field">Add field</string>
<string name="entry_add_attachment">Add attachment</string>
<string name="content_description_remove_field">Remove field</string>
@@ -204,6 +205,7 @@
<string name="hint_length">Length</string>
<string name="hint_pass">Password</string>
<string name="password">Password</string>
<string name="passphrase">Passphrase</string>
<string name="invalid_credentials">Could not read credentials.</string>
<string name="invalid_algorithm">Wrong algorithm.</string>
<string name="invalid_db_same_uuid">%1$s with the same UUID %2$s already exists.</string>
@@ -225,6 +227,8 @@
<string name="lowercase">Lower-case</string>
<string name="hide_password_title">Hide passwords</string>
<string name="hide_password_summary">Mask passwords (***) by default</string>
<string name="colorize_password_title">Colorize passwords</string>
<string name="colorize_password_summary">Colorize password characters by type</string>
<string name="about">About</string>
<string name="menu_change_key_settings">Change master key</string>
<string name="copy_field">Copy of %1$s</string>
@@ -603,6 +607,23 @@
<string name="unit_kibibyte">KiB</string>
<string name="unit_mebibyte">MiB</string>
<string name="unit_gibibyte">GiB</string>
<string name="entropy">Entropy: %1$s bit</string>
<string name="entropy_high">Entropy: High</string>
<string name="entropy_calculate">Entropy: Calculate…</string>
<string name="at_least_one_char">At least one character from each</string>
<string name="exclude_ambiguous_chars">Exclude ambiguous characters</string>
<string name="consider_chars_filter">Consider characters</string>
<string name="word_separator">Separator</string>
<string name="ignore_chars_filter">Ignore characters</string>
<string name="lower_case">lower case</string>
<string name="upper_case">UPPER CASE</string>
<string name="title_case">Title Case</string>
<string name="character_count">Character count: %1$d</string>
<string-array name="word_case_array">
<item>@string/lower_case</item>
<item>@string/upper_case</item>
<item>@string/title_case</item>
</string-array>
<string-array name="timeout_options">
<item>5 seconds</item>
<item>10 seconds</item>

View File

@@ -87,6 +87,22 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/password">
<SwitchPreference
android:key="@string/hide_password_key"
android:title="@string/hide_password_title"
android:summary="@string/hide_password_summary"
android:defaultValue="@bool/hide_password_default"/>
<SwitchPreference
android:key="@string/colorize_password_key"
android:title="@string/colorize_password_title"
android:summary="@string/colorize_password_summary"
android:defaultValue="@bool/colorize_password_default"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/other">

View File

@@ -93,32 +93,6 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/password">
<SeekBarPreference
android:key="@string/password_length_key"
android:title="@string/password_size_title"
android:summary="@string/password_size_summary"
android:defaultValue="@string/default_password_length"
app:min="@string/min_password_length"
app:showSeekBarValue="true"
android:max="@string/max_password_length" />
<MultiSelectListPreference
android:key="@string/list_password_generator_options_key"
android:title="@string/list_password_generator_options_title"
android:summary="@string/list_password_generator_options_summary"
android:entries="@array/list_password_generator_options_entries"
android:entryValues="@array/list_password_generator_options_values"
android:defaultValue="@array/list_password_generator_options_default_values"/>
<SwitchPreference
android:key="@string/hide_password_key"
android:title="@string/hide_password_title"
android:summary="@string/hide_password_summary"
android:defaultValue="@bool/hide_password_default"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/database_history">

View File

@@ -0,0 +1,6 @@
* Show visual password strength indicator with entropy #631 #869
* Dynamically save password generator configuration #618 #696
* Add advanced password filters #1052
* Add editable chars fields #539
* Add color for special password chars #454
* Passphrase implementation #218

View File

@@ -0,0 +1,6 @@
* Affichage d'un indicateur visuel de la force du mot de passe avec entropie #631 #869
* Sauvegarde dynamique de la configuration du générateur de mots de passe #618 #696
* Ajout des filtres de mots de passe avancés #1052
* Ajout de champs éditable de génération #539
* Ajout des couleurs pour chaque caractère spécial de mots de passe #454
* Phrases secrètes #218