Add advanced password filters #1052

This commit is contained in:
J-Jamet
2022-03-28 21:26:26 +02:00
parent dcf61fd4e2
commit a55488846b
8 changed files with 273 additions and 117 deletions

View File

@@ -1,6 +1,7 @@
KeePassDX(3.4.0)
* Show visual password strength indicator with entropy #631 #869
* Dynamically save password generator configuration #618
* Add advanced password filters #1052
KeePassDX(3.3.3)
* Fix shared otpauth link if database not open #1274

View File

@@ -57,6 +57,8 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
private var specialsCompound: CompoundButton? = null
private var bracketsCompound: CompoundButton? = null
private var extendedCompound: CompoundButton? = null
private var atLeastOneCompound: CompoundButton? = null
private var excludeAmbiguousCompound: CompoundButton? = null
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -102,6 +104,8 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
specialsCompound = root?.findViewById(R.id.special_filter)
bracketsCompound = root?.findViewById(R.id.brackets_filter)
extendedCompound = root?.findViewById(R.id.extendedASCII_filter)
atLeastOneCompound = root?.findViewById(R.id.atLeastOne_filter)
excludeAmbiguousCompound = root?.findViewById(R.id.excludeAmbiguous_filter)
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
@@ -134,6 +138,12 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
extendedCompound?.setOnCheckedChangeListener { _, _ ->
fillPassword()
}
atLeastOneCompound?.setOnCheckedChangeListener { _, _ ->
fillPassword()
}
excludeAmbiguousCompound?.setOnCheckedChangeListener { _, _ ->
fillPassword()
}
var listenSlider = true
var listenEditText = true
@@ -228,6 +238,10 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
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())
}
@@ -243,6 +257,8 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
specialsCompound?.isChecked = false
bracketsCompound?.isChecked = false
extendedCompound?.isChecked = false
atLeastOneCompound?.isChecked = false
excludeAmbiguousCompound?.isChecked = false
context?.let { context ->
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
@@ -257,6 +273,8 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
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
}
}
}
@@ -306,7 +324,9 @@ class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
spaceCompound?.isChecked == true,
specialsCompound?.isChecked == true,
bracketsCompound?.isChecked == true,
extendedCompound?.isChecked == true)
extendedCompound?.isChecked == true,
atLeastOneCompound?.isChecked == true,
excludeAmbiguousCompound?.isChecked == true)
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a password", e)
}

View File

@@ -22,9 +22,201 @@ package com.kunzisoft.keepass.password
import android.content.res.Resources
import com.kunzisoft.keepass.R
import java.security.SecureRandom
import java.util.*
class PasswordGenerator(private val resources: Resources) {
@Throws(IllegalArgumentException::class)
fun generatePassword(length: Int,
upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean,
atLeastOneFromEach: Boolean,
excludeAmbiguousChar: Boolean): String {
// Desired password length is 0 or less
if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
}
// No option has been checked
if (!upperCase
&& !lowerCase
&& !digits
&& !minus
&& !underline
&& !space
&& !specials
&& !brackets
&& !extended) {
throw IllegalArgumentException(resources.getString(R.string.error_pass_gen_type))
}
// Filter builder
val passwordFilters = PasswordFilters().apply {
this.length = length
if (upperCase) {
addFilter(
Filter(
if (excludeAmbiguousChar) UPPERCASE_NON_AMBIGUOUS_CHARS else UPPERCASE_CHARS,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (lowerCase) {
addFilter(
Filter(
if (excludeAmbiguousChar) LOWERCASE_NON_AMBIGUOUS_CHARS else LOWERCASE_CHARS,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (digits) {
addFilter(
Filter(
if (excludeAmbiguousChar) DIGIT_NON_AMBIGUOUS_CHARS else DIGIT_CHARS,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (minus) {
addFilter(
Filter(
MINUS_CHAR,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (underline) {
addFilter(
Filter(
UNDERLINE_CHAR,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (space) {
addFilter(
Filter(
SPACE_CHAR,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (specials) {
addFilter(
Filter(
SPECIAL_CHARS,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (brackets) {
addFilter(
Filter(
BRACKET_CHARS,
if (atLeastOneFromEach) 1 else 0
)
)
}
if (extended) {
addFilter(
Filter(
extendedChars(),
if (atLeastOneFromEach) 1 else 0
)
)
}
}
return generateRandomString(SecureRandom(), passwordFilters)
}
private fun generateRandomString(random: Random, passwordFilters: PasswordFilters): String {
val randomString = StringBuilder()
// Allocate appropriate memory for the password.
var requiredCharactersLeft = passwordFilters.getRequiredCharactersLeft()
// Build the password.
for (i in 0 until passwordFilters.length) {
val 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()
}
// Now that the string is built, get the next random character.
val selectableCharsMaxIndex = selectableChars.length - 1
val randomSelectableCharsIndex = if (selectableCharsMaxIndex > 0) random.nextInt(selectableCharsMaxIndex) else 0
val nextChar = selectableChars[randomSelectableCharsIndex]
// Put at random position
val randomStringMaxIndex = randomString.length - 1
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--
}
}
}
return randomString.toString()
}
private data class Filter(var chars: String,
var minCharsNeeded: Int)
private class PasswordFilters {
var length: Int = 0
val filters = mutableListOf<Filter>()
fun addFilter(filter: Filter) {
filters.add(filter)
}
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()
}
}
// From KeePassXC code https://github.com/keepassxreboot/keepassxc/pull/538
private fun extendedChars(): String {
val charSet = StringBuilder()
@@ -47,105 +239,13 @@ class PasswordGenerator(private val resources: Resources) {
return charSet.toString()
}
@Throws(IllegalArgumentException::class)
fun generatePassword(length: Int,
upperCase: Boolean,
lowerCase: Boolean,
digits: Boolean,
minus: Boolean,
underline: Boolean,
space: Boolean,
specials: Boolean,
brackets: Boolean,
extended: Boolean): String {
// Desired password length is 0 or less
if (length <= 0) {
throw IllegalArgumentException(resources.getString(R.string.error_wrong_length))
}
// No option has been checked
if (!upperCase
&& !lowerCase
&& !digits
&& !minus
&& !underline
&& !space
&& !specials
&& !brackets
&& !extended) {
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)])
}
}
return buffer.toString()
}
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()
if (upperCase) {
charSet.append(UPPERCASE_CHARS)
}
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 charSet.toString()
}
companion object {
private const val UPPERCASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private const val UPPERCASE_NON_AMBIGUOUS_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ"
private const val LOWERCASE_CHARS = "abcdefghijklmnopqrstuvwxyz"
private const val LOWERCASE_NON_AMBIGUOUS_CHARS = "abcdefghjkmnpqrstuvwxyz"
private const val DIGIT_CHARS = "0123456789"
private const val DIGIT_NON_AMBIGUOUS_CHARS = "23456789"
private const val MINUS_CHAR = "-"
private const val UNDERLINE_CHAR = "_"
private const val SPACE_CHAR = " "

View File

@@ -39,30 +39,30 @@
android:layout_marginTop="@dimen/card_view_margin_vertical"
android:layout_marginBottom="@dimen/card_view_margin_vertical"
app:cardCornerRadius="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/card_view_padding">
<com.kunzisoft.keepass.view.PasswordView
android:id="@+id/password_view"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/password_copy_button" />
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"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
<ScrollView
@@ -195,17 +195,46 @@
android:text="@string/visual_extended"/>
</com.google.android.material.chip.ChipGroup>
<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: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:checked="true"
android:text="@string/exclude_ambiguous_chars"/>
</com.google.android.material.chip.ChipGroup>
<Button
android:id="@+id/generate_password_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.Button"
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>
</androidx.cardview.widget.CardView>
</ScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/generate_password_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:fabSize="mini"
android:layout_margin="8dp"
android:contentDescription="@string/generate_password"
android:src="@drawable/ic_generate_password_white_24dp"/>
</LinearLayout>

View File

@@ -379,6 +379,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>

View File

@@ -606,6 +606,8 @@
<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-array name="timeout_options">
<item>5 seconds</item>
<item>10 seconds</item>

View File

@@ -1,2 +1,3 @@
* Show visual password strength indicator with entropy #631 #869
* Dynamically save password generator configuration #618
* Dynamically save password generator configuration #618
* Add advanced password filters #1052

View File

@@ -1,2 +1,3 @@
* 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
* Sauvegarde dynamique de la configuration du générateur de mots de passe #618
* Ajout des filtres de mots de passe avancés #1052