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