Generate passphrase #218

This commit is contained in:
J-Jamet
2022-04-01 19:08:50 +02:00
parent f607b35cf3
commit 76f9e8ec6e
22 changed files with 1128 additions and 380 deletions

View File

@@ -4,6 +4,7 @@ KeePassDX(3.4.0)
* 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

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

@@ -1,328 +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.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.activities.fragments.DatabaseFragment
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.PasswordView
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
class PasswordGeneratorFragment : DatabaseFragment() {
private var passwordView: PasswordView? = null
private var sliderLength: Slider? = null
private var lengthEditView: EditText? = null
private var uppercaseCompound: CompoundButton? = null
private var lowercaseCompound: CompoundButton? = null
private var digitsCompound: CompoundButton? = null
private var minusCompound: CompoundButton? = null
private var underlineCompound: CompoundButton? = null
private var spaceCompound: CompoundButton? = null
private var specialsCompound: CompoundButton? = null
private var bracketsCompound: CompoundButton? = null
private var extendedCompound: CompoundButton? = null
private var considerCharsEditText: EditText? = null
private var ignoreCharsEditText: EditText? = null
private var atLeastOneCompound: CompoundButton? = null
private var excludeAmbiguousCompound: CompoundButton? = null
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)
passwordView = 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(passwordView!!.passwordString,
getString(R.string.copy_field,
getString(R.string.entry_password)))
}
}
assignDefaultCharacters()
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.keyGeneratedValidated.observe(viewLifecycleOwner) {
mKeyGeneratorViewModel.setKeyGenerated(passwordView?.passwordString ?: "")
}
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
generatePassword()
}
resetAppTimeoutWhenViewFocusedOrChanged(view)
}
override fun onDestroy() {
saveOptions()
super.onDestroy()
}
override fun onDatabaseRetrieved(database: Database?) {
// Nothing here
}
private fun saveOptions() {
context?.let {
val optionsSet = mutableSetOf<String>()
if (uppercaseCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_uppercase))
if (lowercaseCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_lowercase))
if (digitsCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_digits))
if (minusCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_minus))
if (underlineCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_underline))
if (spaceCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_space))
if (specialsCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_special))
if (bracketsCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_brackets))
if (extendedCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_extended))
if (atLeastOneCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_atLeastOne))
if (excludeAmbiguousCompound?.isChecked == true)
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
PreferencesUtil.setDefaultPasswordCharacters(it, optionsSet)
PreferencesUtil.setDefaultPasswordLength(it, getPasswordLength())
}
}
private fun assignDefaultCharacters() {
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
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
for (passwordChar in charSet) {
when (passwordChar) {
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
}
}
}
val defaultPasswordLength = PreferencesUtil.getDefaultPasswordLength(context)
setSliderValue(defaultPasswordLength)
lengthEditView?.setText(defaultPasswordLength.toString())
}
}
private fun getPasswordLength(): Int {
return try {
Integer.valueOf(lengthEditView?.text.toString())
} catch (numberException: NumberFormatException) {
MIN_SLIDER_LENGTH.toInt()
}
}
private fun setSliderValue(value: Int) {
val sliderValue = value.toFloat()
when {
sliderValue < MIN_SLIDER_LENGTH -> {
sliderLength?.value = MIN_SLIDER_LENGTH
}
sliderValue > MAX_SLIDER_LENGTH -> {
sliderLength?.value = MAX_SLIDER_LENGTH
}
else -> {
sliderLength?.value = sliderValue
}
}
}
private fun generatePassword() {
var password = ""
try {
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
uppercaseCompound?.isChecked == true,
lowercaseCompound?.isChecked == true,
digitsCompound?.isChecked == true,
minusCompound?.isChecked == true,
underlineCompound?.isChecked == true,
spaceCompound?.isChecked == true,
specialsCompound?.isChecked == true,
bracketsCompound?.isChecked == true,
extendedCompound?.isChecked == true,
considerCharsEditText?.text?.toString() ?: "",
ignoreCharsEditText?.text?.toString() ?: "",
atLeastOneCompound?.isChecked == true,
excludeAmbiguousCompound?.isChecked == true)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}
passwordView?.passwordString = password
}
companion object {
private const val MIN_SLIDER_LENGTH = 1F
private const val MAX_SLIDER_LENGTH = 64F
private const val TAG = "PasswordGeneratorFrgmt"
}
}

View File

@@ -39,7 +39,7 @@ 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.PasswordView
import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
@@ -51,7 +51,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var passwordCheckBox: CompoundButton? = null
private var passwordView: PasswordView? = null
private var passKeyView: PassKeyView? = null
private var passwordRepeatTextInputLayout: TextInputLayout? = null
private var passwordRepeatView: TextView? = null
@@ -133,7 +133,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
passwordView = rootView?.findViewById(R.id.password_view)
passKeyView = rootView?.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility()
@@ -204,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!!.passwordString
mMasterPassword = passKeyView!!.passwordString
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match

View File

@@ -1,3 +1,22 @@
/*
* 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
@@ -5,12 +24,14 @@ 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() {
@@ -18,6 +39,17 @@ class KeyGeneratorFragment : DatabaseFragment() {
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?,
@@ -36,6 +68,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
}.attach()
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
resetAppTimeoutWhenViewFocusedOrChanged(view)
@@ -45,6 +78,33 @@ class KeyGeneratorFragment : DatabaseFragment() {
}
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?) {

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

@@ -2,14 +2,15 @@ package com.kunzisoft.keepass.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.kunzisoft.keepass.activities.dialogs.PasswordGeneratorFragment
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 = PasswordGeneratorFragment()
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
override fun getItemCount(): Int {
return KeyGeneratorFragment.KeyGeneratorTab.values().size

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

@@ -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.*
@@ -204,37 +205,120 @@ 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 setDefaultPasswordLength(context: Context, passwordLength: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.password_length_key),
context.getString(R.string.password_generator_length_key),
passwordLength
)
apply()
}
}
fun getDefaultPasswordCharacters(context: Context): Set<String>? {
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 setDefaultPasswordCharacters(context: Context, passwordOptionsSet: Set<String>) {
fun setDefaultPasswordOptions(context: Context, passwordOptionsSet: Set<String>) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putStringSet(
context.getString(R.string.list_password_generator_options_key),
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 {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.clipboard_notifications_key),
@@ -635,8 +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.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())

View File

@@ -35,9 +35,9 @@ import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
class PassKeyView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy? = null
@@ -47,6 +47,8 @@ class PasswordView @JvmOverloads constructor(context: Context,
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()
@@ -74,10 +76,13 @@ class PasswordView @JvmOverloads constructor(context: Context,
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.PasswordView,
R.styleable.PassKeyView,
0, 0).apply {
try {
mShowPassword = getBoolean(R.styleable.PasswordView_passwordVisible,
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()
@@ -85,15 +90,17 @@ class PasswordView @JvmOverloads constructor(context: Context,
}
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_password, this)
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)

View File

@@ -1,3 +1,22 @@
/*
* 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
@@ -11,10 +30,19 @@ class KeyGeneratorViewModel: ViewModel() {
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
}
@@ -23,7 +51,23 @@ class KeyGeneratorViewModel: ViewModel() {
_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,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

@@ -31,10 +31,10 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="154dp">
android:minHeight="180dp">
<View
android:layout_width="match_parent"
android:layout_height="112dp"
android:layout_height="120dp"
android:background="?attr/colorPrimary" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
@@ -52,7 +52,7 @@
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
<com.kunzisoft.keepass.view.PasswordView
<com.kunzisoft.keepass.view.PassKeyView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -111,9 +111,9 @@
android:layout_toLeftOf="@+id/length"
android:contentDescription="@string/content_description_password_length"
android:stepSize="1"
android:value="@string/default_password_length"
android:valueFrom="@string/min_password_length"
android:valueTo="@string/max_password_length"
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" />
@@ -122,13 +122,11 @@
android:id="@+id/length"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_below="@+id/length_label"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:inputType="number"
android:maxLength="3"
android:maxLines="1"
android:text="@string/default_password_length" />
android:maxLines="1" />
</RelativeLayout>

View File

@@ -71,11 +71,11 @@
android:text="@string/password"/>
<!-- Password Input -->
<com.kunzisoft.keepass.view.PasswordView
<com.kunzisoft.keepass.view.PassKeyView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordVisible="false"/>
app:passKeyVisible="false"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_repeat_input_layout"
android:layout_width="match_parent"

View File

@@ -41,8 +41,10 @@
<attr name="explanations" format="string" />
</declare-styleable>
<declare-styleable name="PasswordView">
<attr name="passwordVisible" format="boolean" />
<declare-styleable name="PassKeyView">
<attr name="passKeyHint" format="string" />
<attr name="passKeyMaxLines" format="integer" />
<attr name="passKeyVisible" format="boolean" />
</declare-styleable>
<!-- Specific keyboard attributes -->

View File

@@ -202,8 +202,22 @@
<string name="reset_education_screens_key" translatable="false">relaunch_education_screens_key</string>
<!-- Password Generator Settings -->
<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="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>
@@ -369,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">64</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>

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>
@@ -612,7 +613,17 @@
<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

@@ -3,3 +3,4 @@
* Add advanced password filters #1052
* Add editable chars fields #539
* Add color for special password chars #454
* Passphrase implementation #218

View File

@@ -3,3 +3,4 @@
* 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