aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/pwgen
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 /app/src/main/java/com/zeapo/pwdstore/pwgen
parente25e0035a2e4d6227f9d8071687e421e784186a4 (diff)
downloadHardenedPass-33b3f54921bba7549933c6f889fbe7ab63b61b7c.tar.gz
HardenedPass-33b3f54921bba7549933c6f889fbe7ab63b61b7c.zip
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>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/pwgen')
-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
5 files changed, 314 insertions, 407 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) }
+ }
+}