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>
This commit is contained in:
Fabian Henneke 2020-06-18 12:04:33 +02:00 committed by GitHub
parent e25e0035a2
commit 33b3f54921
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 414 additions and 554 deletions

View file

@ -7,139 +7,131 @@ package com.zeapo.pwdstore.pwgen
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import com.zeapo.pwdstore.R 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 { object PasswordGenerator {
internal const val DIGITS = 0x0001 const val DEFAULT_LENGTH = 16
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
internal const val DIGITS_STR = "0123456789" const val DIGITS = 0x0001
internal const val UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" const val UPPERS = 0x0002
internal const val LOWERS_STR = "abcdefghijklmnopqrstuvwxyz" const val SYMBOLS = 0x0004
internal const val SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" const val NO_AMBIGUOUS = 0x0008
internal const val AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2" const val LOWERS = 0x0020
internal const val VOWELS_STR = "01aeiouyAEIOUY"
// No a, c, n, h, H, C, 1, N const val DIGITS_STR = "0123456789"
private const val pwOptions = "0ABsvyL" const val UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const val LOWERS_STR = "abcdefghijklmnopqrstuvwxyz"
const val SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
const val AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2"
/** /**
* Sets password generation preferences. * Enables the [PasswordOption]s in [options] and sets [targetLength] as the length for
* * generated passwords.
* @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
*/ */
@JvmStatic fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean {
fun setPrefs(ctx: Context, argv: ArrayList<String>, vararg numArgv: Int): Boolean {
ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit { ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit {
for (option in pwOptions.toCharArray()) { for (possibleOption in PasswordOption.values())
if (argv.contains(option.toString())) { putBoolean(possibleOption.key, possibleOption in options)
putBoolean(option.toString(), true) putInt("length", targetLength)
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++
}
} }
return true 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 * Generates a password using the preferences set by [setPrefs].
* [.setPrefs].
*
* @param ctx context from which to retrieve SharedPreferences from
* preferences file 'PasswordGenerator'
* @return list of generated passwords
*/ */
@JvmStatic @Throws(PasswordGeneratorException::class)
@Throws(PasswordGeneratorExeption::class) fun generate(ctx: Context): String {
fun generate(ctx: Context): ArrayList<String> {
val prefs = ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) val prefs = ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
var numCharacterCategories = 0
var phonemes = true var phonemes = true
var pwgenFlags = DIGITS or UPPERS or LOWERS var pwgenFlags = DIGITS or UPPERS or LOWERS
for (option in pwOptions.toCharArray()) { for (option in PasswordOption.values()) {
if (prefs.getBoolean(option.toString(), false)) { if (prefs.getBoolean(option.key, false)) {
when (option) { when (option) {
'0' -> pwgenFlags = pwgenFlags and DIGITS.inv() PasswordOption.NoDigits -> pwgenFlags = pwgenFlags.clearFlag(DIGITS)
'A' -> pwgenFlags = pwgenFlags and UPPERS.inv() PasswordOption.NoUppercaseLetters -> pwgenFlags = pwgenFlags.clearFlag(UPPERS)
'L' -> pwgenFlags = pwgenFlags and LOWERS.inv() PasswordOption.NoLowercaseLetters -> pwgenFlags = pwgenFlags.clearFlag(LOWERS)
'B' -> pwgenFlags = pwgenFlags or AMBIGUOUS PasswordOption.NoAmbiguousCharacters -> pwgenFlags = pwgenFlags or NO_AMBIGUOUS
's' -> phonemes = false PasswordOption.FullyRandom -> phonemes = false
'y' -> pwgenFlags = pwgenFlags or SYMBOLS PasswordOption.AtLeastOneSymbol -> {
'v' -> { numCharacterCategories++
phonemes = false pwgenFlags = pwgenFlags or SYMBOLS
pwgenFlags = pwgenFlags or NO_VOWELS // | DIGITS | UPPERS;
} }
} // pwgenFlags = DIGITS | UPPERS; }
}
}
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))
}
if (length < numCategories) {
throw PasswordGeneratorExeption(ctx.resources.getString(R.string.pwgen_length_too_short_error))
}
if ((pwgenFlags and UPPERS) == 0 && (pwgenFlags and LOWERS) == 0) { // Only digits and/or symbols
phonemes = false
pwgenFlags = pwgenFlags and AMBIGUOUS.inv()
} else if (length < 5) {
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))
} else { } else {
passwords.add(RandomPasswordGenerator.rand(length, pwgenFlags)) // 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 -> {
}
}
} }
} }
return passwords
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 < numCharacterCategories) {
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_length_too_short_error))
}
if (!(pwgenFlags hasFlag UPPERS) && !(pwgenFlags hasFlag LOWERS)) {
phonemes = false
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
}
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 {
RandomPasswordGenerator.generate(length, pwgenFlags)
}
} while (password == null)
return password
} }
class PasswordGeneratorExeption(string: String) : Exception(string) class PasswordGeneratorException(string: String) : Exception(string)
} }

View file

@ -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
}
}

View file

@ -4,27 +4,30 @@
*/ */
package com.zeapo.pwdstore.pwgen package com.zeapo.pwdstore.pwgen
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
internal object RandomNumberGenerator { private val secureRandom = SecureRandom()
private var random: SecureRandom
init { /**
try { * Returns a number between 0 (inclusive) and [exclusiveBound] (exclusive).
random = SecureRandom.getInstance("SHA1PRNG") */
} catch (e: NoSuchAlgorithmException) { fun secureRandomNumber(exclusiveBound: Int) = secureRandom.nextInt(exclusiveBound)
throw SecurityException("SHA1PRNG not available", e)
}
}
/** /**
* Generate a random number n, where 0 &lt;= n &lt; maxNum. * Returns `true` and `false` with probablity 50% each.
* */
* @param maxNum the bound on the random number to be returned fun secureRandomBoolean() = secureRandom.nextBoolean()
* @return the generated random number
*/ /**
fun number(maxNum: Int): Int { * Returns `true` with probability [percentTrue]% and `false` with probability
return random.nextInt(maxNum) * `(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)]

View file

@ -4,77 +4,43 @@
*/ */
package com.zeapo.pwdstore.pwgen 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 * - [PasswordGenerator.DIGITS]: If set, the password will contain at least one digit; if not
* @param pwFlags flag field where set bits indicate conditions the * set, the password will not contain any digits.
* generated password must meet * - [PasswordGenerator.UPPERS]: If set, the password will contain at least one uppercase
* <table summary ="bits of flag field"> * letter; if not set, the password will not contain any uppercase letters.
* <tr><td>Bit</td><td>Condition</td></tr> * - [PasswordGenerator.LOWERS]: If set, the password will contain at least one lowercase
* <tr><td>0</td><td>include at least one number</td></tr> * letter; if not set, the password will not contain any lowercase letters.
* <tr><td>1</td><td>include at least one uppercase letter</td></tr> * - [PasswordGenerator.SYMBOLS]: If set, the password will contain at least one symbol; if not
* <tr><td>2</td><td>include at least one symbol</td></tr> * set, the password will not contain any symbols.
* <tr><td>3</td><td>don't include ambiguous characters</td></tr> * - [PasswordGenerator.NO_AMBIGUOUS]: If set, the password will not contain any ambiguous
* <tr><td>4</td><td>don't include vowels</td></tr> * characters.
* <tr><td>5</td><td>include at least one lowercase</td></tr> * - [PasswordGenerator.NO_VOWELS]: If set, the password will not contain any vowels.
</table> *
* @return the generated password
*/ */
fun rand(size: Int, pwFlags: Int): String { fun generate(targetLength: Int, pwFlags: Int): String? {
var password: String val bank = listOfNotNull(
var cha: Char PasswordGenerator.DIGITS_STR.takeIf { pwFlags hasFlag PasswordGenerator.DIGITS },
var i: Int PasswordGenerator.UPPERS_STR.takeIf { pwFlags hasFlag PasswordGenerator.UPPERS },
var featureFlags: Int PasswordGenerator.LOWERS_STR.takeIf { pwFlags hasFlag PasswordGenerator.LOWERS },
var num: Int PasswordGenerator.SYMBOLS_STR.takeIf { pwFlags hasFlag PasswordGenerator.SYMBOLS }
var character: String ).joinToString("")
var bank = "" var password = ""
if (pwFlags and PasswordGenerator.DIGITS > 0) { while (password.length < targetLength) {
bank += PasswordGenerator.DIGITS_STR val candidate = bank.secureRandomCharacter()
} if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
if (pwFlags and PasswordGenerator.UPPERS > 0) { candidate in PasswordGenerator.AMBIGUOUS_STR) {
bank += PasswordGenerator.UPPERS_STR continue
}
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()
}
} }
} while (featureFlags and (PasswordGenerator.UPPERS or PasswordGenerator.DIGITS or PasswordGenerator.SYMBOLS or PasswordGenerator.LOWERS) > 0) password += candidate
return password }
return password.takeIf { PasswordGenerator.isValidPassword(it, pwFlags) }
} }
} }

View file

@ -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) }
}
}

View file

@ -6,11 +6,11 @@ package com.zeapo.pwdstore.pwgenxkpwd
import android.content.Context import android.content.Context
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.pwgen.PasswordGenerator import com.zeapo.pwdstore.pwgen.PasswordGenerator.PasswordGeneratorException
import com.zeapo.pwdstore.pwgen.PasswordGenerator.PasswordGeneratorExeption import com.zeapo.pwdstore.pwgen.secureRandomCharacter
import com.zeapo.pwdstore.pwgen.secureRandomElement
import com.zeapo.pwdstore.pwgen.secureRandomNumber
import java.io.IOException import java.io.IOException
import java.security.SecureRandom
import java.util.ArrayList
import java.util.Locale import java.util.Locale
class PasswordBuilder(ctx: Context) { class PasswordBuilder(ctx: Context) {
@ -67,29 +67,25 @@ class PasswordBuilder(ctx: Context) {
} }
private fun generateRandomNumberSequence(totalNumbers: Int): String { private fun generateRandomNumberSequence(totalNumbers: Int): String {
val secureRandom = SecureRandom()
val numbers = StringBuilder(totalNumbers) val numbers = StringBuilder(totalNumbers)
for (i in 0 until totalNumbers) { for (i in 0 until totalNumbers) {
numbers.append(secureRandom.nextInt(10)) numbers.append(secureRandomNumber(10))
} }
return numbers.toString() return numbers.toString()
} }
private fun generateRandomSymbolSequence(numSymbols: Int): String { private fun generateRandomSymbolSequence(numSymbols: Int): String {
val secureRandom = SecureRandom()
val numbers = StringBuilder(numSymbols) val numbers = StringBuilder(numSymbols)
for (i in 0 until numSymbols) { for (i in 0 until numSymbols) {
numbers.append(SYMBOLS[secureRandom.nextInt(SYMBOLS.length)]) numbers.append(SYMBOLS.secureRandomCharacter())
} }
return numbers.toString() return numbers.toString()
} }
@Throws(PasswordGenerator.PasswordGeneratorExeption::class) @OptIn(ExperimentalStdlibApi::class)
@Throws(PasswordGeneratorException::class)
fun create(): String { fun create(): String {
val wordBank = ArrayList<String>() val wordBank = mutableListOf<String>()
val secureRandom = SecureRandom()
val password = StringBuilder() val password = StringBuilder()
if (prependDigits != 0) { if (prependDigits != 0) {
@ -101,44 +97,30 @@ class PasswordBuilder(ctx: Context) {
try { try {
val dictionary = XkpwdDictionary(context) val dictionary = XkpwdDictionary(context)
val words = dictionary.words val words = dictionary.words
for (wordLength in words.keys) { for (wordLength in minWordLength..maxWordLength) {
if (wordLength in minWordLength..maxWordLength) { wordBank.addAll(words[wordLength] ?: emptyList())
wordBank.addAll(words[wordLength]!!)
}
} }
if (wordBank.size == 0) { 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) { for (i in 0 until numWords) {
val randomIndex = secureRandom.nextInt(wordBank.size) val candidate = wordBank.secureRandomElement()
var s = wordBank[randomIndex] val s = when (capsType) {
CapsType.UPPERCASE -> candidate.toUpperCase(Locale.getDefault())
if (capsType != CapsType.As_iS) { CapsType.Sentencecase -> if (i == 0) candidate.capitalize(Locale.getDefault()) else candidate
s = s.toLowerCase(Locale.getDefault()) CapsType.TitleCase -> candidate.capitalize(Locale.getDefault())
when (capsType) { CapsType.lowercase -> candidate.toLowerCase(Locale.getDefault())
CapsType.UPPERCASE -> s = s.toUpperCase(Locale.getDefault()) CapsType.As_iS -> candidate
CapsType.Sentencecase -> {
if (i == 0) {
s = capitalize(s)
}
}
CapsType.TitleCase -> {
s = capitalize(s)
}
CapsType.lowercase, CapsType.As_iS -> {
}
}
} }
password.append(s) password.append(s)
wordBank.removeAt(randomIndex)
if (i + 1 < numWords) { if (i + 1 < numWords) {
password.append(separator) password.append(separator)
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
throw PasswordGeneratorExeption("Failed generating password!") throw PasswordGeneratorException("Failed generating password!")
} }
if (numDigits != 0) { if (numDigits != 0) {
if (isAppendNumberSeparator) { if (isAppendNumberSeparator) {
@ -155,13 +137,6 @@ class PasswordBuilder(ctx: Context) {
return password.toString() 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 { companion object {
private const val SYMBOLS = "!@\$%^&*-_+=:|~?/.;#" private const val SYMBOLS = "!@\$%^&*-_+=:|~?/.;#"
} }

View file

@ -5,51 +5,29 @@
package com.zeapo.pwdstore.pwgenxkpwd package com.zeapo.pwdstore.pwgenxkpwd
import android.content.Context import android.content.Context
import android.text.TextUtils
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import java.io.File import java.io.File
import java.util.ArrayList
import java.util.HashMap
class XkpwdDictionary(context: Context) { class XkpwdDictionary(context: Context) {
val words: HashMap<Int, ArrayList<String>> = HashMap() val words: Map<Int, List<String>>
init { init {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val uri = prefs.getString("pref_key_custom_dict", "")!!
val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
var lines: List<String> = listOf() val lines = if (prefs.getBoolean("pref_key_is_custom_dict", false) &&
uri.isNotEmpty() && customDictFile.canRead()) {
if (prefs.getBoolean("pref_key_is_custom_dict", false)) { customDictFile.readLines()
} else {
val uri = prefs.getString("pref_key_custom_dict", "") context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines()
if (!TextUtils.isEmpty(uri)) {
val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
if (customDictFile.exists() && customDictFile.canRead()) {
lines = customDictFile.inputStream().bufferedReader().readLines()
}
}
} }
if (lines.isEmpty()) { words = lines.asSequence()
lines = context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines() .map { it.trim() }
} .filter { it.isNotEmpty() && !it.contains(' ') }
.groupBy { it.length }
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 <= ' ' })
}
}
}
} }
companion object { companion object {

View file

@ -4,6 +4,8 @@
*/ */
package com.zeapo.pwdstore.ui.dialogs package com.zeapo.pwdstore.ui.dialogs
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.graphics.Typeface import android.graphics.Typeface
@ -11,94 +13,87 @@ import android.os.Bundle
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.annotation.IdRes
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R 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.generate
import com.zeapo.pwdstore.pwgen.PasswordGenerator.setPrefs import com.zeapo.pwdstore.pwgen.PasswordGenerator.setPrefs
import com.zeapo.pwdstore.pwgen.PasswordOption
/** A placeholder fragment containing a simple view. */
class PasswordGeneratorDialogFragment : DialogFragment() { class PasswordGeneratorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireContext())
val callingActivity = requireActivity() val callingActivity = requireActivity()
val inflater = callingActivity.layoutInflater val inflater = callingActivity.layoutInflater
@SuppressLint("InflateParams")
val view = inflater.inflate(R.layout.fragment_pwgen, null) val view = inflater.inflate(R.layout.fragment_pwgen, null)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
builder.setView(view)
val prefs = requireActivity().applicationContext val prefs = requireActivity().applicationContext
.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) .getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
view.findViewById<CheckBox>(R.id.numerals)?.isChecked = !prefs.getBoolean("0", false) view.findViewById<CheckBox>(R.id.numerals)?.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false)
view.findViewById<CheckBox>(R.id.symbols)?.isChecked = prefs.getBoolean("y", false) view.findViewById<CheckBox>(R.id.symbols)?.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false)
view.findViewById<CheckBox>(R.id.uppercase)?.isChecked = !prefs.getBoolean("A", false) view.findViewById<CheckBox>(R.id.uppercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false)
view.findViewById<CheckBox>(R.id.lowercase)?.isChecked = !prefs.getBoolean("L", false) view.findViewById<CheckBox>(R.id.lowercase)?.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false)
view.findViewById<CheckBox>(R.id.ambiguous)?.isChecked = !prefs.getBoolean("B", false) view.findViewById<CheckBox>(R.id.ambiguous)?.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false)
view.findViewById<CheckBox>(R.id.pronounceable)?.isChecked = !prefs.getBoolean("s", true) view.findViewById<CheckBox>(R.id.pronounceable)?.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true)
val textView: AppCompatEditText = view.findViewById(R.id.lengthNumber) val textView: AppCompatEditText = view.findViewById(R.id.lengthNumber)
textView.setText(prefs.getInt("length", 20).toString()) textView.setText(prefs.getInt("length", 20).toString())
val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText) val passwordText: AppCompatTextView = view.findViewById(R.id.passwordText)
passwordText.typeface = monoTypeface passwordText.typeface = monoTypeface
builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> return MaterialAlertDialogBuilder(requireContext()).run {
val edit = callingActivity.findViewById<EditText>(R.id.password) setTitle(R.string.pwgen_title)
edit.setText(passwordText.text) setView(view)
} setPositiveButton(R.string.dialog_ok) { _, _ ->
builder.setNeutralButton(resources.getString(R.string.dialog_cancel)) { _, _ -> } val edit = callingActivity.findViewById<EditText>(R.id.password)
builder.setNegativeButton(resources.getString(R.string.pwgen_generate), null) edit.setText(passwordText.text)
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 = ""
} }
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { setNeutralButton(R.string.dialog_cancel) { _, _ -> }
setPreferences() setNegativeButton(R.string.pwgen_generate, null)
try { create()
passwordText.text = generate(callingActivity.applicationContext)[0] }.apply {
} catch (e: PasswordGeneratorExeption) { setOnShowListener {
Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() generate(passwordText)
passwordText.text = "" getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
generate(passwordText)
} }
} }
} }
return dialog }
private fun generate(passwordField: AppCompatTextView) {
setPreferences()
try {
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() { private fun setPreferences() {
val preferences = ArrayList<String>() val preferences = listOfNotNull(
if (!(dialog!!.findViewById<CheckBox>(R.id.numerals)).isChecked) { PasswordOption.NoDigits.takeIf { !isChecked(R.id.numerals) },
preferences.add("0") PasswordOption.AtLeastOneSymbol.takeIf { isChecked(R.id.symbols) },
} PasswordOption.NoUppercaseLetters.takeIf { !isChecked(R.id.uppercase) },
if ((dialog!!.findViewById<CheckBox>(R.id.symbols)).isChecked) { PasswordOption.NoAmbiguousCharacters.takeIf { !isChecked(R.id.ambiguous) },
preferences.add("y") PasswordOption.FullyRandom.takeIf { !isChecked(R.id.pronounceable) },
} PasswordOption.NoLowercaseLetters.takeIf { !isChecked(R.id.lowercase) }
if (!(dialog!!.findViewById<CheckBox>(R.id.uppercase)).isChecked) { )
preferences.add("A") val lengthText = requireDialog().findViewById<EditText>(R.id.lengthNumber).text.toString()
} val length = lengthText.toIntOrNull()?.takeIf { it >= 0 }
if (!(dialog!!.findViewById<CheckBox>(R.id.ambiguous)).isChecked) { ?: PasswordGenerator.DEFAULT_LENGTH
preferences.add("B") setPrefs(requireActivity().applicationContext, preferences, length)
}
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)
try {
val length = Integer.valueOf(editText.text.toString())
setPrefs(requireActivity().applicationContext, preferences, length)
} catch (e: NumberFormatException) {
setPrefs(requireActivity().applicationContext, preferences)
}
} }
} }

View file

@ -124,7 +124,7 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
.appendNumbers(if (cbNumbers.isChecked) Integer.parseInt(spinnerNumbersCount.selectedItem as String) else 0) .appendNumbers(if (cbNumbers.isChecked) Integer.parseInt(spinnerNumbersCount.selectedItem as String) else 0)
.appendSymbols(if (cbSymbols.isChecked) Integer.parseInt(spinnerSymbolsCount.selectedItem as String) else 0) .appendSymbols(if (cbSymbols.isChecked) Integer.parseInt(spinnerSymbolsCount.selectedItem as String) else 0)
.setCapitalization(CapsType.valueOf(spinnerCapsType.selectedItem.toString())).create() .setCapitalization(CapsType.valueOf(spinnerCapsType.selectedItem.toString())).create()
} catch (e: PasswordGenerator.PasswordGeneratorExeption) { } catch (e: PasswordGenerator.PasswordGeneratorException) {
Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show()
tag("xkpw").e(e, "failure generating xkpasswd") tag("xkpw").e(e, "failure generating xkpasswd")
passwordText.text = FALLBACK_ERROR_PASS passwordText.text = FALLBACK_ERROR_PASS

View file

@ -29,6 +29,10 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirect
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import java.io.File import java.io.File
fun Int.clearFlag(flag: Int): Int {
return this and flag.inv()
}
infix fun Int.hasFlag(flag: Int): Boolean { infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag return this and flag == flag
} }

View file

@ -183,6 +183,7 @@
<string name="pwgen_pronounceable">Pronounceable</string> <string name="pwgen_pronounceable">Pronounceable</string>
<string name="pwgen_no_chars_error">No characters included</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_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 --> <!-- XKPWD password generator -->
<string name="xkpwgen_title">Xkpasswd Generator</string> <string name="xkpwgen_title">Xkpasswd Generator</string>