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 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;
}
}
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 {
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
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)
/**
* 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` and `false` with probablity 50% each.
*/
fun secureRandomBoolean() = secureRandom.nextBoolean()
/**
* 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)]

View file

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

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 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 = "!@\$%^&*-_+=:|~?/.;#"
}

View file

@ -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)
val uri = prefs.getString("pref_key_custom_dict", "")!!
val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
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 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 {

View file

@ -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 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() {
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)
try {
val length = Integer.valueOf(editText.text.toString())
setPrefs(requireActivity().applicationContext, preferences, length)
} catch (e: NumberFormatException) {
setPrefs(requireActivity().applicationContext, preferences)
}
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)
}
}

View file

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

View file

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

View file

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