summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-06-18 12:04:33 +0200
committerGitHub <noreply@github.com>2020-06-18 12:04:33 +0200
commit33b3f54921bba7549933c6f889fbe7ab63b61b7c (patch)
tree0c79a1bf93a0094657fc497efc80ae2c1affb518
parente25e0035a2e4d6227f9d8071687e421e784186a4 (diff)
Refactor password generation (#860)
* Refactor password generation * Update Extensions.kt * Update app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt Co-authored-by: Harsh Shandilya <me@msfjarvis.dev> * Address review comments Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt194
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgen/Phonemes.kt221
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgen/RandomNumberGenerator.kt41
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt98
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPhonemesGenerator.kt167
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/PasswordBuilder.kt65
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/XkpwdDictionary.kt48
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt111
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt4
-rw-r--r--app/src/main/res/values/strings.xml1
11 files changed, 406 insertions, 546 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt b/app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt
index 9f7faa56..b4accec7 100644
--- a/app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/PasswordGenerator.kt
@@ -7,139 +7,131 @@ package com.zeapo.pwdstore.pwgen
import android.content.Context
import androidx.core.content.edit
import com.zeapo.pwdstore.R
-import java.util.ArrayList
+import com.zeapo.pwdstore.utils.clearFlag
+import com.zeapo.pwdstore.utils.hasFlag
+
+enum class PasswordOption(val key: String) {
+ NoDigits("0"),
+ NoUppercaseLetters("A"),
+ NoAmbiguousCharacters("B"),
+ FullyRandom("s"),
+ AtLeastOneSymbol("y"),
+ NoLowercaseLetters("L")
+}
object PasswordGenerator {
- internal const val DIGITS = 0x0001
- internal const val UPPERS = 0x0002
- internal const val SYMBOLS = 0x0004
- internal const val AMBIGUOUS = 0x0008
- internal const val NO_VOWELS = 0x0010
- internal const val LOWERS = 0x0020
+ const val DEFAULT_LENGTH = 16
- internal const val DIGITS_STR = "0123456789"
- internal const val UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- internal const val LOWERS_STR = "abcdefghijklmnopqrstuvwxyz"
- internal const val SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
- internal const val AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2"
- internal const val VOWELS_STR = "01aeiouyAEIOUY"
+ const val DIGITS = 0x0001
+ const val UPPERS = 0x0002
+ const val SYMBOLS = 0x0004
+ const val NO_AMBIGUOUS = 0x0008
+ const val LOWERS = 0x0020
- // No a, c, n, h, H, C, 1, N
- private const val pwOptions = "0ABsvyL"
+ const val DIGITS_STR = "0123456789"
+ const val UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ const val LOWERS_STR = "abcdefghijklmnopqrstuvwxyz"
+ const val SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
+ const val AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2"
/**
- * Sets password generation preferences.
- *
- * @param ctx context from which to retrieve SharedPreferences from
- * preferences file 'PasswordGenerator'
- * @param argv options for password generation
- * <table summary="options for password generation">
- * <tr><td>Option</td><td>Description</td></tr>
- * <tr><td>0</td><td>don't include numbers</td></tr>
- * <tr><td>A</td><td>don't include uppercase letters</td></tr>
- * <tr><td>B</td><td>don't include ambiguous charactersl</td></tr>
- * <tr><td>s</td><td>generate completely random passwords</td></tr>
- * <tr><td>v</td><td>don't include vowels</td></tr>
- * <tr><td>y</td><td>include at least one symbol</td></tr>
- * <tr><td>L</td><td>don't include lowercase letters</td></tr>
- </table> *
- * @param numArgv numerical options for password generation: length of
- * generated passwords followed by number of passwords to
- * generate
- * @return `false` if a numerical options is invalid,
- * `true` otherwise
+ * Enables the [PasswordOption]s in [options] and sets [targetLength] as the length for
+ * generated passwords.
*/
- @JvmStatic
- fun setPrefs(ctx: Context, argv: ArrayList<String>, vararg numArgv: Int): Boolean {
+ fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean {
ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit {
- for (option in pwOptions.toCharArray()) {
- if (argv.contains(option.toString())) {
- putBoolean(option.toString(), true)
- argv.remove(option.toString())
- } else {
- putBoolean(option.toString(), false)
- }
- }
- var i = 0
- while (i < numArgv.size && i < 2) {
- if (numArgv[i] <= 0) {
- // Invalid password length or number of passwords
- return false
- }
- val name = if (i == 0) "length" else "num"
- putInt(name, numArgv[i])
- i++
- }
+ for (possibleOption in PasswordOption.values())
+ putBoolean(possibleOption.key, possibleOption in options)
+ putInt("length", targetLength)
}
return true
}
+ fun isValidPassword(password: String, pwFlags: Int): Boolean {
+ if (pwFlags hasFlag DIGITS && password.none { it in DIGITS_STR })
+ return false
+ if (pwFlags hasFlag UPPERS && password.none { it in UPPERS_STR })
+ return false
+ if (pwFlags hasFlag LOWERS && password.none { it in LOWERS_STR })
+ return false
+ if (pwFlags hasFlag SYMBOLS && password.none { it in SYMBOLS_STR })
+ return false
+ if (pwFlags hasFlag NO_AMBIGUOUS && password.any { it in AMBIGUOUS_STR })
+ return false
+ return true
+ }
+
/**
- * Generates passwords using the preferences set by
- * [.setPrefs].
- *
- * @param ctx context from which to retrieve SharedPreferences from
- * preferences file 'PasswordGenerator'
- * @return list of generated passwords
+ * Generates a password using the preferences set by [setPrefs].
*/
- @JvmStatic
- @Throws(PasswordGeneratorExeption::class)
- fun generate(ctx: Context): ArrayList<String> {
+ @Throws(PasswordGeneratorException::class)
+ fun generate(ctx: Context): String {
val prefs = ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
+ var numCharacterCategories = 0
var phonemes = true
var pwgenFlags = DIGITS or UPPERS or LOWERS
- for (option in pwOptions.toCharArray()) {
- if (prefs.getBoolean(option.toString(), false)) {
+ for (option in PasswordOption.values()) {
+ if (prefs.getBoolean(option.key, false)) {
when (option) {
- '0' -> pwgenFlags = pwgenFlags and DIGITS.inv()
- 'A' -> pwgenFlags = pwgenFlags and UPPERS.inv()
- 'L' -> pwgenFlags = pwgenFlags and LOWERS.inv()
- 'B' -> pwgenFlags = pwgenFlags or AMBIGUOUS
- 's' -> phonemes = false
- 'y' -> pwgenFlags = pwgenFlags or SYMBOLS
- 'v' -> {
- phonemes = false
- pwgenFlags = pwgenFlags or NO_VOWELS // | DIGITS | UPPERS;
+ PasswordOption.NoDigits -> pwgenFlags = pwgenFlags.clearFlag(DIGITS)
+ PasswordOption.NoUppercaseLetters -> pwgenFlags = pwgenFlags.clearFlag(UPPERS)
+ PasswordOption.NoLowercaseLetters -> pwgenFlags = pwgenFlags.clearFlag(LOWERS)
+ PasswordOption.NoAmbiguousCharacters -> pwgenFlags = pwgenFlags or NO_AMBIGUOUS
+ PasswordOption.FullyRandom -> phonemes = false
+ PasswordOption.AtLeastOneSymbol -> {
+ numCharacterCategories++
+ pwgenFlags = pwgenFlags or SYMBOLS
}
- } // pwgenFlags = DIGITS | UPPERS;
+ }
+ } else {
+ // The No* options are false, so the respective character category will be included.
+ when (option) {
+ PasswordOption.NoDigits,
+ PasswordOption.NoUppercaseLetters,
+ PasswordOption.NoLowercaseLetters -> {
+ numCharacterCategories++
+ }
+ PasswordOption.NoAmbiguousCharacters,
+ PasswordOption.FullyRandom,
+ // Since AtLeastOneSymbol is not negated, it is counted in the if branch.
+ PasswordOption.AtLeastOneSymbol -> {
+ }
+ }
}
}
- val length = prefs.getInt("length", 8)
- var numCategories = 0
- var categories = pwgenFlags and AMBIGUOUS.inv()
-
- while (categories != 0) {
- if (categories and 1 == 1)
- numCategories++
- categories = categories shr 1
- }
- if (numCategories == 0) {
- throw PasswordGeneratorExeption(ctx.resources.getString(R.string.pwgen_no_chars_error))
+ val length = prefs.getInt("length", DEFAULT_LENGTH)
+ if (pwgenFlags.clearFlag(NO_AMBIGUOUS) == 0) {
+ throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_no_chars_error))
}
- if (length < numCategories) {
- throw PasswordGeneratorExeption(ctx.resources.getString(R.string.pwgen_length_too_short_error))
+ if (length < numCharacterCategories) {
+ throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_length_too_short_error))
}
- if ((pwgenFlags and UPPERS) == 0 && (pwgenFlags and LOWERS) == 0) { // Only digits and/or symbols
+ if (!(pwgenFlags hasFlag UPPERS) && !(pwgenFlags hasFlag LOWERS)) {
phonemes = false
- pwgenFlags = pwgenFlags and AMBIGUOUS.inv()
- } else if (length < 5) {
+ pwgenFlags = pwgenFlags.clearFlag(NO_AMBIGUOUS)
+ }
+ // Experiments show that phonemes may require more than 1000 iterations to generate a valid
+ // password if the length is not at least 6.
+ if (length < 6) {
phonemes = false
}
- val passwords = ArrayList<String>()
- val num = prefs.getInt("num", 1)
- for (i in 0 until num) {
- if (phonemes) {
- passwords.add(Phonemes.phonemes(length, pwgenFlags))
+ var password: String?
+ var iterations = 0
+ do {
+ if (iterations++ > 1000)
+ throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_max_iterations_exceeded))
+ password = if (phonemes) {
+ RandomPhonemesGenerator.generate(length, pwgenFlags)
} else {
- passwords.add(RandomPasswordGenerator.rand(length, pwgenFlags))
+ RandomPasswordGenerator.generate(length, pwgenFlags)
}
- }
- return passwords
+ } while (password == null)
+ return password
}
- class PasswordGeneratorExeption(string: String) : Exception(string)
+ class PasswordGeneratorException(string: String) : Exception(string)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/Phonemes.kt b/app/src/main/java/com/zeapo/pwdstore/pwgen/Phonemes.kt
deleted file mode 100644
index c570ff1c..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/pwgen/Phonemes.kt
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.pwgen
-
-internal object Phonemes {
- private const val CONSONANT = 0x0001
- private const val VOWEL = 0x0002
- private const val DIPHTHONG = 0x0004
- private const val NOT_FIRST = 0x0008
-
- private val elements = arrayOf(
- Element("a", VOWEL),
- Element("ae", VOWEL or DIPHTHONG),
- Element("ah", VOWEL or DIPHTHONG),
- Element("ai", VOWEL or DIPHTHONG),
- Element("b", CONSONANT),
- Element("c", CONSONANT),
- Element("ch", CONSONANT or DIPHTHONG),
- Element("d", CONSONANT),
- Element("e", VOWEL),
- Element("ee", VOWEL or DIPHTHONG),
- Element("ei", VOWEL or DIPHTHONG),
- Element("f", CONSONANT),
- Element("g", CONSONANT),
- Element("gh", CONSONANT or DIPHTHONG or NOT_FIRST),
- Element("h", CONSONANT),
- Element("i", VOWEL),
- Element("ie", VOWEL or DIPHTHONG),
- Element("j", CONSONANT),
- Element("k", CONSONANT),
- Element("l", CONSONANT),
- Element("m", CONSONANT),
- Element("n", CONSONANT),
- Element("ng", CONSONANT or DIPHTHONG or NOT_FIRST),
- Element("o", VOWEL),
- Element("oh", VOWEL or DIPHTHONG),
- Element("oo", VOWEL or DIPHTHONG),
- Element("p", CONSONANT),
- Element("ph", CONSONANT or DIPHTHONG),
- Element("qu", CONSONANT or DIPHTHONG),
- Element("r", CONSONANT),
- Element("s", CONSONANT),
- Element("sh", CONSONANT or DIPHTHONG),
- Element("t", CONSONANT),
- Element("th", CONSONANT or DIPHTHONG),
- Element("u", VOWEL),
- Element("v", CONSONANT),
- Element("w", CONSONANT),
- Element("x", CONSONANT),
- Element("y", CONSONANT),
- Element("z", CONSONANT)
- )
-
- private val NUM_ELEMENTS = elements.size
-
- private class Element internal constructor(internal var str: String, internal var flags: Int)
-
- /**
- * Generates a human-readable password.
- *
- * @param size length of password to generate
- * @param pwFlags flag field where set bits indicate conditions the
- * generated password must meet
- * <table summary="bits of flag field">
- * <tr><td>Bit</td><td>Condition</td></tr>
- * <tr><td>0</td><td>include at least one number</td></tr>
- * <tr><td>1</td><td>include at least one uppercase letter</td></tr>
- * <tr><td>2</td><td>include at least one symbol</td></tr>
- * <tr><td>3</td><td>don't include ambiguous characters</td></tr>
- * <tr><td>5</td><td>include at least one lowercase letter</td></tr>
- </table> *
- * @return the generated password
- */
- fun phonemes(size: Int, pwFlags: Int): String {
- var password: String
- var curSize: Int
- var i: Int
- var length: Int
- var flags: Int
- var featureFlags: Int
- var prev: Int
- var shouldBe: Int
- var first: Boolean
- var str: String
- var cha: Char
-
- do {
- password = ""
- featureFlags = pwFlags
- curSize = 0
- prev = 0
- first = true
-
- shouldBe = if (RandomNumberGenerator.number(2) == 1) VOWEL else CONSONANT
-
- while (curSize < size) {
- i = RandomNumberGenerator.number(NUM_ELEMENTS)
- str = elements[i].str
- length = str.length
- flags = elements[i].flags
- // Filter on the basic type of the next Element
- if (flags and shouldBe == 0) {
- continue
- }
- // Handle the NOT_FIRST flag
- if (first && flags and NOT_FIRST > 0) {
- continue
- }
- // Don't allow VOWEL followed a Vowel/Diphthong pair
- if (prev and VOWEL > 0 && flags and VOWEL > 0 &&
- flags and DIPHTHONG > 0
- ) {
- continue
- }
- // Don't allow us to overflow the buffer
- if (length > size - curSize) {
- continue
- }
- // OK, we found an Element which matches our criteria, let's do
- // it
- password += str
-
- // Handle UPPERS
- if (pwFlags and PasswordGenerator.UPPERS > 0) {
- if ((pwFlags and PasswordGenerator.LOWERS == 0) ||
- (first || flags and CONSONANT > 0) && RandomNumberGenerator.number(10) < 2) {
- val index = password.length - length
- password = password.substring(0, index) + str.toUpperCase()
- featureFlags = featureFlags and PasswordGenerator.UPPERS.inv()
- }
- }
-
- // Handle the AMBIGUOUS flag
- if (pwFlags and PasswordGenerator.AMBIGUOUS > 0) {
- for (ambiguous in PasswordGenerator.AMBIGUOUS_STR.toCharArray()) {
- if (password.contains(ambiguous.toString())) {
- password = password.substring(0, curSize)
-
- // Still have upper letters
- if ((pwFlags and PasswordGenerator.UPPERS) > 0) {
- featureFlags = featureFlags or PasswordGenerator.UPPERS
- for (upper in PasswordGenerator.UPPERS_STR.toCharArray()) {
- if (password.contains(upper.toString())) {
- featureFlags = featureFlags and PasswordGenerator.UPPERS.inv()
- break
- }
- }
- }
- break
- }
- }
- if (password.length == curSize)
- continue
- }
-
- curSize += length
-
- // Time to stop?
- if (curSize >= size)
- break
-
- // Handle DIGITS
- if (pwFlags and PasswordGenerator.DIGITS > 0) {
- if (!first && RandomNumberGenerator.number(10) < 3) {
- var character: String
- do {
- cha = Character.forDigit(RandomNumberGenerator.number(10), 10)
- character = cha.toString()
- } while (pwFlags and PasswordGenerator.AMBIGUOUS > 0 &&
- PasswordGenerator.AMBIGUOUS_STR.contains(character))
- password += character
- curSize++
-
- featureFlags = featureFlags and PasswordGenerator.DIGITS.inv()
-
- first = true
- prev = 0
- shouldBe = if (RandomNumberGenerator.number(2) == 1) VOWEL else CONSONANT
- continue
- }
- }
-
- // Handle SYMBOLS
- if (pwFlags and PasswordGenerator.SYMBOLS > 0) {
- if (!first && RandomNumberGenerator.number(10) < 2) {
- var character: String
- var num: Int
- do {
- num = RandomNumberGenerator.number(PasswordGenerator.SYMBOLS_STR.length)
- cha = PasswordGenerator.SYMBOLS_STR.toCharArray()[num]
- character = cha.toString()
- } while (pwFlags and PasswordGenerator.AMBIGUOUS > 0 &&
- PasswordGenerator.AMBIGUOUS_STR.contains(character))
- password += character
- curSize++
-
- featureFlags = featureFlags and PasswordGenerator.SYMBOLS.inv()
- }
- }
-
- // OK, figure out what the next Element should be
- shouldBe = if (shouldBe == CONSONANT) {
- VOWEL
- } else {
- if (prev and VOWEL > 0 || flags and DIPHTHONG > 0 ||
- RandomNumberGenerator.number(10) > 3
- ) {
- CONSONANT
- } else {
- VOWEL
- }
- }
- prev = flags
- first = false
- }
- } while (featureFlags and (PasswordGenerator.UPPERS or PasswordGenerator.DIGITS or PasswordGenerator.SYMBOLS) > 0)
- return password
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomNumberGenerator.kt b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomNumberGenerator.kt
index 698b3574..6c4c956e 100644
--- a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomNumberGenerator.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomNumberGenerator.kt
@@ -4,27 +4,30 @@
*/
package com.zeapo.pwdstore.pwgen
-import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
-internal object RandomNumberGenerator {
- private var random: SecureRandom
+private val secureRandom = SecureRandom()
- init {
- try {
- random = SecureRandom.getInstance("SHA1PRNG")
- } catch (e: NoSuchAlgorithmException) {
- throw SecurityException("SHA1PRNG not available", e)
- }
- }
+/**
+ * Returns a number between 0 (inclusive) and [exclusiveBound] (exclusive).
+ */
+fun secureRandomNumber(exclusiveBound: Int) = secureRandom.nextInt(exclusiveBound)
+
+/**
+ * Returns `true` and `false` with probablity 50% each.
+ */
+fun secureRandomBoolean() = secureRandom.nextBoolean()
- /**
- * Generate a random number n, where 0 &lt;= n &lt; maxNum.
- *
- * @param maxNum the bound on the random number to be returned
- * @return the generated random number
- */
- fun number(maxNum: Int): Int {
- return random.nextInt(maxNum)
- }
+/**
+ * Returns `true` with probability [percentTrue]% and `false` with probability
+ * `(100 - [percentTrue])`%.
+ */
+fun secureRandomBiasedBoolean(percentTrue: Int): Boolean {
+ require(1 <= percentTrue) { "Probability for returning `true` must be at least 1%" }
+ require(percentTrue <= 99) { "Probability for returning `true` must be at most 99%" }
+ return secureRandomNumber(100) < percentTrue
}
+
+fun <T> Array<T>.secureRandomElement() = this[secureRandomNumber(size)]
+fun <T> List<T>.secureRandomElement() = this[secureRandomNumber(size)]
+fun String.secureRandomCharacter() = this[secureRandomNumber(length)]
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt
index bd79f16a..e841ed76 100644
--- a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPasswordGenerator.kt
@@ -4,77 +4,43 @@
*/
package com.zeapo.pwdstore.pwgen
-internal object RandomPasswordGenerator {
+import com.zeapo.pwdstore.utils.hasFlag
+
+object RandomPasswordGenerator {
/**
- * Generates a completely random password.
+ * Generates a random password of length [targetLength], taking the following flags in [pwFlags]
+ * into account, or fails to do so and returns null:
*
- * @param size length of password to generate
- * @param pwFlags flag field where set bits indicate conditions the
- * generated password must meet
- * <table summary ="bits of flag field">
- * <tr><td>Bit</td><td>Condition</td></tr>
- * <tr><td>0</td><td>include at least one number</td></tr>
- * <tr><td>1</td><td>include at least one uppercase letter</td></tr>
- * <tr><td>2</td><td>include at least one symbol</td></tr>
- * <tr><td>3</td><td>don't include ambiguous characters</td></tr>
- * <tr><td>4</td><td>don't include vowels</td></tr>
- * <tr><td>5</td><td>include at least one lowercase</td></tr>
- </table> *
- * @return the generated password
+ * - [PasswordGenerator.DIGITS]: If set, the password will contain at least one digit; if not
+ * set, the password will not contain any digits.
+ * - [PasswordGenerator.UPPERS]: If set, the password will contain at least one uppercase
+ * letter; if not set, the password will not contain any uppercase letters.
+ * - [PasswordGenerator.LOWERS]: If set, the password will contain at least one lowercase
+ * letter; if not set, the password will not contain any lowercase letters.
+ * - [PasswordGenerator.SYMBOLS]: If set, the password will contain at least one symbol; if not
+ * set, the password will not contain any symbols.
+ * - [PasswordGenerator.NO_AMBIGUOUS]: If set, the password will not contain any ambiguous
+ * characters.
+ * - [PasswordGenerator.NO_VOWELS]: If set, the password will not contain any vowels.
*/
- fun rand(size: Int, pwFlags: Int): String {
- var password: String
- var cha: Char
- var i: Int
- var featureFlags: Int
- var num: Int
- var character: String
+ fun generate(targetLength: Int, pwFlags: Int): String? {
+ val bank = listOfNotNull(
+ PasswordGenerator.DIGITS_STR.takeIf { pwFlags hasFlag PasswordGenerator.DIGITS },
+ PasswordGenerator.UPPERS_STR.takeIf { pwFlags hasFlag PasswordGenerator.UPPERS },
+ PasswordGenerator.LOWERS_STR.takeIf { pwFlags hasFlag PasswordGenerator.LOWERS },
+ PasswordGenerator.SYMBOLS_STR.takeIf { pwFlags hasFlag PasswordGenerator.SYMBOLS }
+ ).joinToString("")
- var bank = ""
- if (pwFlags and PasswordGenerator.DIGITS > 0) {
- bank += PasswordGenerator.DIGITS_STR
- }
- if (pwFlags and PasswordGenerator.UPPERS > 0) {
- bank += PasswordGenerator.UPPERS_STR
- }
- if (pwFlags and PasswordGenerator.LOWERS > 0) {
- bank += PasswordGenerator.LOWERS_STR
- }
- if (pwFlags and PasswordGenerator.SYMBOLS > 0) {
- bank += PasswordGenerator.SYMBOLS_STR
- }
- do {
- password = ""
- featureFlags = pwFlags
- i = 0
- while (i < size) {
- num = RandomNumberGenerator.number(bank.length)
- cha = bank.toCharArray()[num]
- character = cha.toString()
- if (pwFlags and PasswordGenerator.AMBIGUOUS > 0 &&
- PasswordGenerator.AMBIGUOUS_STR.contains(character)) {
- continue
- }
- if (pwFlags and PasswordGenerator.NO_VOWELS > 0 && PasswordGenerator.VOWELS_STR.contains(character)) {
- continue
- }
- password += character
- i++
- if (PasswordGenerator.DIGITS_STR.contains(character)) {
- featureFlags = featureFlags and PasswordGenerator.DIGITS.inv()
- }
- if (PasswordGenerator.UPPERS_STR.contains(character)) {
- featureFlags = featureFlags and PasswordGenerator.UPPERS.inv()
- }
- if (PasswordGenerator.SYMBOLS_STR.contains(character)) {
- featureFlags = featureFlags and PasswordGenerator.SYMBOLS.inv()
- }
- if (PasswordGenerator.LOWERS_STR.contains(character)) {
- featureFlags = featureFlags and PasswordGenerator.LOWERS.inv()
- }
+ var password = ""
+ while (password.length < targetLength) {
+ val candidate = bank.secureRandomCharacter()
+ if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
+ candidate in PasswordGenerator.AMBIGUOUS_STR) {
+ continue
}
- } while (featureFlags and (PasswordGenerator.UPPERS or PasswordGenerator.DIGITS or PasswordGenerator.SYMBOLS or PasswordGenerator.LOWERS) > 0)
- return password
+ password += candidate
+ }
+ return password.takeIf { PasswordGenerator.isValidPassword(it, pwFlags) }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPhonemesGenerator.kt b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPhonemesGenerator.kt
new file mode 100644
index 00000000..de375422
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/pwgen/RandomPhonemesGenerator.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.pwgen
+
+import com.zeapo.pwdstore.utils.hasFlag
+import java.util.Locale
+
+object RandomPhonemesGenerator {
+ private const val CONSONANT = 0x0001
+ private const val VOWEL = 0x0002
+ private const val DIPHTHONG = 0x0004
+ private const val NOT_FIRST = 0x0008
+
+ private val elements = arrayOf(
+ Element("a", VOWEL),
+ Element("ae", VOWEL or DIPHTHONG),
+ Element("ah", VOWEL or DIPHTHONG),
+ Element("ai", VOWEL or DIPHTHONG),
+ Element("b", CONSONANT),
+ Element("c", CONSONANT),
+ Element("ch", CONSONANT or DIPHTHONG),
+ Element("d", CONSONANT),
+ Element("e", VOWEL),
+ Element("ee", VOWEL or DIPHTHONG),
+ Element("ei", VOWEL or DIPHTHONG),
+ Element("f", CONSONANT),
+ Element("g", CONSONANT),
+ Element("gh", CONSONANT or DIPHTHONG or NOT_FIRST),
+ Element("h", CONSONANT),
+ Element("i", VOWEL),
+ Element("ie", VOWEL or DIPHTHONG),
+ Element("j", CONSONANT),
+ Element("k", CONSONANT),
+ Element("l", CONSONANT),
+ Element("m", CONSONANT),
+ Element("n", CONSONANT),
+ Element("ng", CONSONANT or DIPHTHONG or NOT_FIRST),
+ Element("o", VOWEL),
+ Element("oh", VOWEL or DIPHTHONG),
+ Element("oo", VOWEL or DIPHTHONG),
+ Element("p", CONSONANT),
+ Element("ph", CONSONANT or DIPHTHONG),
+ Element("qu", CONSONANT or DIPHTHONG),
+ Element("r", CONSONANT),
+ Element("s", CONSONANT),
+ Element("sh", CONSONANT or DIPHTHONG),
+ Element("t", CONSONANT),
+ Element("th", CONSONANT or DIPHTHONG),
+ Element("u", VOWEL),
+ Element("v", CONSONANT),
+ Element("w", CONSONANT),
+ Element("x", CONSONANT),
+ Element("y", CONSONANT),
+ Element("z", CONSONANT)
+ )
+
+ private class Element(str: String, val flags: Int) {
+ val upperCase = str.toUpperCase(Locale.ROOT)
+ val lowerCase = str.toLowerCase(Locale.ROOT)
+ val length = str.length
+ val isAmbiguous = str.any { it in PasswordGenerator.AMBIGUOUS_STR }
+ }
+
+ /**
+ * Generates a random human-readable password of length [targetLength], taking the following
+ * flags in [pwFlags] into account, or fails to do so and returns null:
+ *
+ * - [PasswordGenerator.DIGITS]: If set, the password will contain at least one digit; if not
+ * set, the password will not contain any digits.
+ * - [PasswordGenerator.UPPERS]: If set, the password will contain at least one uppercase
+ * letter; if not set, the password will not contain any uppercase letters.
+ * - [PasswordGenerator.LOWERS]: If set, the password will contain at least one lowercase
+ * letter; if not set and [PasswordGenerator.UPPERS] is set, the password will not contain any
+ * lowercase characters; if both are not set, an exception is thrown.
+ * - [PasswordGenerator.SYMBOLS]: If set, the password will contain at least one symbol; if not
+ * set, the password will not contain any symbols.
+ * - [PasswordGenerator.NO_AMBIGUOUS]: If set, the password will not contain any ambiguous
+ * characters.
+ */
+ fun generate(targetLength: Int, pwFlags: Int): String? {
+ require(pwFlags hasFlag PasswordGenerator.UPPERS || pwFlags hasFlag PasswordGenerator.LOWERS)
+
+ var password = ""
+
+ var isStartOfPart = true
+ var nextBasicType = if (secureRandomBoolean()) VOWEL else CONSONANT
+ var previousFlags = 0
+
+ while (password.length < targetLength) {
+ // First part: Add a single letter or pronounceable pair of letters in varying case.
+
+ val candidate = elements.secureRandomElement()
+
+ // Reroll if the candidate does not fulfill the current requirements.
+ if (!candidate.flags.hasFlag(nextBasicType) ||
+ (isStartOfPart && candidate.flags hasFlag NOT_FIRST) ||
+ // Don't let a diphthong that starts with a vowel follow a vowel.
+ (previousFlags hasFlag VOWEL && candidate.flags hasFlag VOWEL && candidate.flags hasFlag DIPHTHONG) ||
+ // Don't add multi-character candidates if we would go over the targetLength.
+ (password.length + candidate.length > targetLength) ||
+ (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && candidate.isAmbiguous)) {
+ continue
+ }
+
+ // At this point the candidate could be appended to the password, but we still have
+ // to determine the case. If no upper case characters are required, we don't add
+ // any.
+ val useUpperIfBothCasesAllowed =
+ (isStartOfPart || candidate.flags hasFlag CONSONANT) && secureRandomBiasedBoolean(20)
+ password += if (pwFlags hasFlag PasswordGenerator.UPPERS &&
+ (!(pwFlags hasFlag PasswordGenerator.LOWERS) || useUpperIfBothCasesAllowed)) {
+ candidate.upperCase
+ } else {
+ candidate.lowerCase
+ }
+
+ // We ensured above that we will not go above the target length.
+ check(password.length <= targetLength)
+ if (password.length == targetLength)
+ break
+
+ // Second part: Add digits and symbols with a certain probability (if requested) if
+ // they would not directly follow the first character in a pronounceable part.
+
+ if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.DIGITS &&
+ secureRandomBiasedBoolean(30)) {
+ var randomDigit: Char
+ do {
+ randomDigit = secureRandomNumber(10).toString(10).first()
+ } while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
+ randomDigit in PasswordGenerator.AMBIGUOUS_STR)
+
+ password += randomDigit
+ // Begin a new pronounceable part after every digit.
+ isStartOfPart = true
+ nextBasicType = if (secureRandomBoolean()) VOWEL else CONSONANT
+ previousFlags = 0
+ continue
+ }
+
+ if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.SYMBOLS &&
+ secureRandomBiasedBoolean(20)) {
+ var randomSymbol: Char
+ do {
+ randomSymbol = PasswordGenerator.SYMBOLS_STR.secureRandomCharacter()
+ } while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
+ randomSymbol in PasswordGenerator.AMBIGUOUS_STR)
+ password += randomSymbol
+ // Continue the password generation as if nothing was added.
+ }
+
+ // Third part: Determine the basic type of the next character depending on the letter
+ // we just added.
+ nextBasicType = when {
+ candidate.flags.hasFlag(CONSONANT) -> VOWEL
+ previousFlags.hasFlag(VOWEL) || candidate.flags.hasFlag(DIPHTHONG) ||
+ secureRandomBiasedBoolean(60) -> CONSONANT
+ else -> VOWEL
+ }
+ previousFlags = candidate.flags
+ isStartOfPart = false
+ }
+ return password.takeIf { PasswordGenerator.isValidPassword(it, pwFlags) }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/PasswordBuilder.kt b/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/PasswordBuilder.kt
index 81aa4362..0f5dcb1c 100644
--- a/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/PasswordBuilder.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/PasswordBuilder.kt
@@ -6,11 +6,11 @@ package com.zeapo.pwdstore.pwgenxkpwd
import android.content.Context
import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.pwgen.PasswordGenerator
-import com.zeapo.pwdstore.pwgen.PasswordGenerator.PasswordGeneratorExeption
+import com.zeapo.pwdstore.pwgen.PasswordGenerator.PasswordGeneratorException
+import com.zeapo.pwdstore.pwgen.secureRandomCharacter
+import com.zeapo.pwdstore.pwgen.secureRandomElement
+import com.zeapo.pwdstore.pwgen.secureRandomNumber
import java.io.IOException
-import java.security.SecureRandom
-import java.util.ArrayList
import java.util.Locale
class PasswordBuilder(ctx: Context) {
@@ -67,29 +67,25 @@ class PasswordBuilder(ctx: Context) {
}
private fun generateRandomNumberSequence(totalNumbers: Int): String {
- val secureRandom = SecureRandom()
val numbers = StringBuilder(totalNumbers)
-
for (i in 0 until totalNumbers) {
- numbers.append(secureRandom.nextInt(10))
+ numbers.append(secureRandomNumber(10))
}
return numbers.toString()
}
private fun generateRandomSymbolSequence(numSymbols: Int): String {
- val secureRandom = SecureRandom()
val numbers = StringBuilder(numSymbols)
-
for (i in 0 until numSymbols) {
- numbers.append(SYMBOLS[secureRandom.nextInt(SYMBOLS.length)])
+ numbers.append(SYMBOLS.secureRandomCharacter())
}
return numbers.toString()
}
- @Throws(PasswordGenerator.PasswordGeneratorExeption::class)
+ @OptIn(ExperimentalStdlibApi::class)
+ @Throws(PasswordGeneratorException::class)
fun create(): String {
- val wordBank = ArrayList<String>()
- val secureRandom = SecureRandom()
+ val wordBank = mutableListOf<String>()
val password = StringBuilder()
if (prependDigits != 0) {
@@ -101,44 +97,30 @@ class PasswordBuilder(ctx: Context) {
try {
val dictionary = XkpwdDictionary(context)
val words = dictionary.words
- for (wordLength in words.keys) {
- if (wordLength in minWordLength..maxWordLength) {
- wordBank.addAll(words[wordLength]!!)
- }
+ for (wordLength in minWordLength..maxWordLength) {
+ wordBank.addAll(words[wordLength] ?: emptyList())
}
if (wordBank.size == 0) {
- throw PasswordGeneratorExeption(context.getString(R.string.xkpwgen_builder_error, minWordLength, maxWordLength))
+ throw PasswordGeneratorException(context.getString(R.string.xkpwgen_builder_error, minWordLength, maxWordLength))
}
for (i in 0 until numWords) {
- val randomIndex = secureRandom.nextInt(wordBank.size)
- var s = wordBank[randomIndex]
-
- if (capsType != CapsType.As_iS) {
- s = s.toLowerCase(Locale.getDefault())
- when (capsType) {
- CapsType.UPPERCASE -> s = s.toUpperCase(Locale.getDefault())
- CapsType.Sentencecase -> {
- if (i == 0) {
- s = capitalize(s)
- }
- }
- CapsType.TitleCase -> {
- s = capitalize(s)
- }
- CapsType.lowercase, CapsType.As_iS -> {
- }
- }
+ val candidate = wordBank.secureRandomElement()
+ val s = when (capsType) {
+ CapsType.UPPERCASE -> candidate.toUpperCase(Locale.getDefault())
+ CapsType.Sentencecase -> if (i == 0) candidate.capitalize(Locale.getDefault()) else candidate
+ CapsType.TitleCase -> candidate.capitalize(Locale.getDefault())
+ CapsType.lowercase -> candidate.toLowerCase(Locale.getDefault())
+ CapsType.As_iS -> candidate
}
password.append(s)
- wordBank.removeAt(randomIndex)
if (i + 1 < numWords) {
password.append(separator)
}
}
} catch (e: IOException) {
- throw PasswordGeneratorExeption("Failed generating password!")
+ throw PasswordGeneratorException("Failed generating password!")
}
if (numDigits != 0) {
if (isAppendNumberSeparator) {
@@ -155,13 +137,6 @@ class PasswordBuilder(ctx: Context) {
return password.toString()
}
- private fun capitalize(s: String): String {
- var result = s
- val lower = result.toLowerCase(Locale.getDefault())
- result = lower.substring(0, 1).toUpperCase(Locale.getDefault()) + result.substring(1)
- return result
- }
-
companion object {
private const val SYMBOLS = "!@\$%^&*-_+=:|~?/.;#"
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/XkpwdDictionary.kt b/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/XkpwdDictionary.kt
index 6a3fbcc3..92438ed0 100644
--- a/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/XkpwdDictionary.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/pwgenxkpwd/XkpwdDictionary.kt
@@ -5,51 +5,29 @@
package com.zeapo.pwdstore.pwgenxkpwd
import android.content.Context
-import android.text.TextUtils
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.R
import java.io.File
-import java.util.ArrayList
-import java.util.HashMap
class XkpwdDictionary(context: Context) {
- val words: HashMap<Int, ArrayList<String>> = HashMap()
+ val words: Map<Int, List<String>>
init {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
-
- var lines: List<String> = listOf()
-
- if (prefs.getBoolean("pref_key_is_custom_dict", false)) {
-
- val uri = prefs.getString("pref_key_custom_dict", "")
-
- if (!TextUtils.isEmpty(uri)) {
- val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
-
- if (customDictFile.exists() && customDictFile.canRead()) {
- lines = customDictFile.inputStream().bufferedReader().readLines()
- }
- }
+ val uri = prefs.getString("pref_key_custom_dict", "")!!
+ val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
+
+ val lines = if (prefs.getBoolean("pref_key_is_custom_dict", false) &&
+ uri.isNotEmpty() && customDictFile.canRead()) {
+ customDictFile.readLines()
+ } else {
+ context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines()
}
- if (lines.isEmpty()) {
- lines = context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines()
- }
-
- for (word in lines) {
- if (!word.trim { it <= ' ' }.contains(" ")) {
- val length = word.trim { it <= ' ' }.length
-
- if (length > 0) {
- if (!words.containsKey(length)) {
- words[length] = ArrayList()
- }
- val strings = words[length]!!
- strings.add(word.trim { it <= ' ' })
- }
- }
- }
+ words = lines.asSequence()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() && !it.contains(' ') }
+ .groupBy { it.length }
}
companion object {
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
index 8f452dca..160f388b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
@@ -4,6 +4,8 @@
*/
package com.zeapo.pwdstore.ui.dialogs
+import android.annotation.SuppressLint
+import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.graphics.Typeface
@@ -11,94 +13,87 @@ import android.os.Bundle
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
+import androidx.annotation.IdRes
import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.AppCompatTextView
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.pwgen.PasswordGenerator.PasswordGeneratorExeption
+import com.zeapo.pwdstore.pwgen.PasswordGenerator
+import com.zeapo.pwdstore.pwgen.PasswordGenerator.PasswordGeneratorException
import com.zeapo.pwdstore.pwgen.PasswordGenerator.generate
import com.zeapo.pwdstore.pwgen.PasswordGenerator.setPrefs
+import com.zeapo.pwdstore.pwgen.PasswordOption
-/** A placeholder fragment containing a simple view. */
class PasswordGeneratorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = MaterialAlertDialogBuilder(requireContext())
val callingActivity = requireActivity()
val inflater = callingActivity.layoutInflater
+
+ @SuppressLint("InflateParams")
val view = inflater.inflate(R.layout.fragment_pwgen, null)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
- builder.setView(view)
val prefs = requireActivity().applicationContext
.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
- view.findViewById<CheckBox>(R.id.numerals)?.isChecked = !prefs.getBoolean("0", false)
- view.findViewById<CheckBox>(R.id.symbols)?.isChecked = prefs.getBoolean("y", false)
- view.findViewById<CheckBox>(R.id.uppercase)?.isChecked = !prefs.getBoolean("A", false)
- view.findViewById<CheckBox>(R.id.lowercase)?.isChecked = !prefs.getBoolean("L", false)
- view.findViewById<CheckBox>(R.id.ambiguous)?.isChecked = !prefs.getBoolean("B", false)
- view.findViewById<CheckBox>(R.id.pronounceable)?.isChecked = !prefs.getBoolean("s", true)
+ view.findViewById<CheckBox>(R.id.numerals)?.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false)
+ view.findViewById<CheckBox>(R.id.symbols)?.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false)
+ view.findViewById<CheckBox>(R.id.uppercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false)
+ view.findViewById<CheckBox>(R.id.lowercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false)
+ view.findViewById<CheckBox>(R.id.ambiguous)?.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false)
+ view.findViewById<CheckBox>(R.id.pronounceable)?.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true)
val textView: AppCompatEditText = view.findViewById(R.id.lengthNumber)
textView.setText(prefs.getInt("length", 20).toString())
val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText)
passwordText.typeface = monoTypeface
- builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
- val edit = callingActivity.findViewById<EditText>(R.id.password)
- edit.setText(passwordText.text)
- }
- builder.setNeutralButton(resources.getString(R.string.dialog_cancel)) { _, _ -> }
- builder.setNegativeButton(resources.getString(R.string.pwgen_generate), null)
- val dialog = builder.setTitle(this.resources.getString(R.string.pwgen_title)).create()
- dialog.setOnShowListener {
- setPreferences()
- try {
- passwordText.text = generate(requireActivity().applicationContext)[0]
- } catch (e: PasswordGeneratorExeption) {
- Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
- passwordText.text = ""
+ return MaterialAlertDialogBuilder(requireContext()).run {
+ setTitle(R.string.pwgen_title)
+ setView(view)
+ setPositiveButton(R.string.dialog_ok) { _, _ ->
+ val edit = callingActivity.findViewById<EditText>(R.id.password)
+ edit.setText(passwordText.text)
}
- dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
- setPreferences()
- try {
- passwordText.text = generate(callingActivity.applicationContext)[0]
- } catch (e: PasswordGeneratorExeption) {
- Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
- passwordText.text = ""
+ setNeutralButton(R.string.dialog_cancel) { _, _ -> }
+ setNegativeButton(R.string.pwgen_generate, null)
+ create()
+ }.apply {
+ setOnShowListener {
+ generate(passwordText)
+ getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
+ generate(passwordText)
}
}
}
- return dialog
}
- private fun setPreferences() {
- val preferences = ArrayList<String>()
- if (!(dialog!!.findViewById<CheckBox>(R.id.numerals)).isChecked) {
- preferences.add("0")
- }
- if ((dialog!!.findViewById<CheckBox>(R.id.symbols)).isChecked) {
- preferences.add("y")
- }
- if (!(dialog!!.findViewById<CheckBox>(R.id.uppercase)).isChecked) {
- preferences.add("A")
- }
- if (!(dialog!!.findViewById<CheckBox>(R.id.ambiguous)).isChecked) {
- preferences.add("B")
- }
- if (!(dialog!!.findViewById<CheckBox>(R.id.pronounceable)).isChecked) {
- preferences.add("s")
- }
- if (!(dialog!!.findViewById<CheckBox>(R.id.lowercase)).isChecked) {
- preferences.add("L")
- }
- val editText = dialog!!.findViewById<EditText>(R.id.lengthNumber)
+ private fun generate(passwordField: AppCompatTextView) {
+ setPreferences()
try {
- val length = Integer.valueOf(editText.text.toString())
- setPrefs(requireActivity().applicationContext, preferences, length)
- } catch (e: NumberFormatException) {
- setPrefs(requireActivity().applicationContext, preferences)
+ passwordField.text = generate(requireContext().applicationContext)
+ } catch (e: PasswordGeneratorException) {
+ Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
+ passwordField.text = ""
}
}
+
+ private fun isChecked(@IdRes id: Int): Boolean {
+ return requireDialog().findViewById<CheckBox>(id).isChecked
+ }
+
+ private fun setPreferences() {
+ val preferences = listOfNotNull(
+ PasswordOption.NoDigits.takeIf { !isChecked(R.id.numerals) },
+ PasswordOption.AtLeastOneSymbol.takeIf { isChecked(R.id.symbols) },
+ PasswordOption.NoUppercaseLetters.takeIf { !isChecked(R.id.uppercase) },
+ PasswordOption.NoAmbiguousCharacters.takeIf { !isChecked(R.id.ambiguous) },
+ PasswordOption.FullyRandom.takeIf { !isChecked(R.id.pronounceable) },
+ PasswordOption.NoLowercaseLetters.takeIf { !isChecked(R.id.lowercase) }
+ )
+ val lengthText = requireDialog().findViewById<EditText>(R.id.lengthNumber).text.toString()
+ val length = lengthText.toIntOrNull()?.takeIf { it >= 0 }
+ ?: PasswordGenerator.DEFAULT_LENGTH
+ setPrefs(requireActivity().applicationContext, preferences, length)
+ }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
index 52bc52c1..206b0533 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/XkPasswordGeneratorDialogFragment.kt
@@ -124,7 +124,7 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
.appendNumbers(if (cbNumbers.isChecked) Integer.parseInt(spinnerNumbersCount.selectedItem as String) else 0)
.appendSymbols(if (cbSymbols.isChecked) Integer.parseInt(spinnerSymbolsCount.selectedItem as String) else 0)
.setCapitalization(CapsType.valueOf(spinnerCapsType.selectedItem.toString())).create()
- } catch (e: PasswordGenerator.PasswordGeneratorExeption) {
+ } catch (e: PasswordGenerator.PasswordGeneratorException) {
Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
tag("xkpw").e(e, "failure generating xkpasswd")
passwordText.text = FALLBACK_ERROR_PASS
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
index ba009bc8..fa22b75e 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
@@ -29,6 +29,10 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirect
import org.eclipse.jgit.api.Git
import java.io.File
+fun Int.clearFlag(flag: Int): Int {
+ return this and flag.inv()
+}
+
infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1583b888..b65d84f2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -183,6 +183,7 @@
<string name="pwgen_pronounceable">Pronounceable</string>
<string name="pwgen_no_chars_error">No characters included</string>
<string name="pwgen_length_too_short_error">Length too short for selected criteria</string>
+ <string name="pwgen_max_iterations_exceeded">Failed to generate a password satisfying the constraints. Try to increase the length.</string>
<!-- XKPWD password generator -->
<string name="xkpwgen_title">Xkpasswd Generator</string>