mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Add advanced password filters #1052
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 = " "
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user