diff options
author | Aditya Wasan <adityawasan55@gmail.com> | 2022-01-13 22:13:53 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-13 16:43:53 +0000 |
commit | abc62c2b6b9d7106eb9f53f4994f086835602e57 (patch) | |
tree | 477942729574acfd0699f090a1e3ad8a073d1703 /app/src/main/java | |
parent | c1ef2e73418d1ddda7711520645a77b8c8e3cf56 (diff) |
Refactor randomized password generator into a separate module (#1663)
Diffstat (limited to 'app/src/main/java')
5 files changed, 50 insertions, 416 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt index ab51cc4e..a21e9a6a 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt @@ -14,6 +14,7 @@ import android.widget.EditText import android.widget.Toast import androidx.annotation.IdRes import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.edit import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult @@ -23,11 +24,12 @@ import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.msfjarvis.aps.R import dev.msfjarvis.aps.databinding.FragmentPwgenBinding +import dev.msfjarvis.aps.passgen.random.MaxIterationsExceededException +import dev.msfjarvis.aps.passgen.random.NoCharactersIncludedException +import dev.msfjarvis.aps.passgen.random.PasswordGenerator +import dev.msfjarvis.aps.passgen.random.PasswordLengthTooShortException +import dev.msfjarvis.aps.passgen.random.PasswordOption import dev.msfjarvis.aps.ui.crypto.PasswordCreationActivity -import dev.msfjarvis.aps.util.pwgen.PasswordGenerator -import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.generate -import dev.msfjarvis.aps.util.pwgen.PasswordGenerator.setPrefs -import dev.msfjarvis.aps.util.pwgen.PasswordOption import dev.msfjarvis.aps.util.settings.PreferenceKeys import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn @@ -96,10 +98,23 @@ class PasswordGeneratorDialogFragment : DialogFragment() { } private fun generate(passwordField: AppCompatTextView) { - setPreferences() + val passwordOptions = getSelectedOptions() + val passwordLength = getLength() + setPrefs(requireContext(), passwordOptions, passwordLength) passwordField.text = - runCatching { generate(requireContext().applicationContext) }.getOrElse { e -> - Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() + runCatching { PasswordGenerator.generate(passwordOptions, passwordLength) }.getOrElse { + exception -> + val errorText = + when (exception) { + is MaxIterationsExceededException -> + requireContext().getString(R.string.pwgen_max_iterations_exceeded) + is NoCharactersIncludedException -> + requireContext().getString(R.string.pwgen_no_chars_error) + is PasswordLengthTooShortException -> + requireContext().getString(R.string.pwgen_length_too_short_error) + else -> requireContext().getString(R.string.pwgen_some_error_occurred) + } + Toast.makeText(requireActivity(), errorText, Toast.LENGTH_SHORT).show() "" } } @@ -108,18 +123,34 @@ class PasswordGeneratorDialogFragment : DialogFragment() { 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) } - ) + private fun getSelectedOptions(): List<PasswordOption> { + return 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) } + ) + } + + private fun getLength(): Int { 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) + return lengthText.toIntOrNull()?.takeIf { it >= 0 } ?: PasswordGenerator.DEFAULT_LENGTH + } + + /** + * Enables the [PasswordOption]s in [options] and sets [targetLength] as the length for generated + * passwords. + */ + private fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean { + ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit { + for (possibleOption in PasswordOption.values()) putBoolean( + possibleOption.key, + possibleOption in options + ) + putInt("length", targetLength) + } + return true } } diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/PasswordGenerator.kt b/app/src/main/java/dev/msfjarvis/aps/util/pwgen/PasswordGenerator.kt deleted file mode 100644 index bd21ea0a..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/PasswordGenerator.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.pwgen - -import android.content.Context -import androidx.core.content.edit -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.util.extensions.clearFlag -import dev.msfjarvis.aps.util.extensions.hasFlag -import dev.msfjarvis.aps.util.settings.PreferenceKeys - -enum class PasswordOption(val key: String) { - NoDigits("0"), - NoUppercaseLetters("A"), - NoAmbiguousCharacters("B"), - FullyRandom("s"), - AtLeastOneSymbol("y"), - NoLowercaseLetters("L") -} - -object PasswordGenerator { - - const val DEFAULT_LENGTH = 16 - - const val DIGITS = 0x0001 - const val UPPERS = 0x0002 - const val SYMBOLS = 0x0004 - const val NO_AMBIGUOUS = 0x0008 - const val LOWERS = 0x0020 - - const val DIGITS_STR = "0123456789" - const val UPPERS_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - const val LOWERS_STR = "abcdefghijklmnopqrstuvwxyz" - const val SYMBOLS_STR = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" - const val AMBIGUOUS_STR = "B8G6I1l0OQDS5Z2" - - /** - * Enables the [PasswordOption] s in [options] and sets [targetLength] as the length for generated - * passwords. - */ - fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean { - ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit { - 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 a password using the preferences set by [setPrefs]. */ - @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 PasswordOption.values()) { - if (prefs.getBoolean(option.key, false)) { - when (option) { - 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 - } - } - } 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(PreferenceKeys.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 PasswordGeneratorException(string: String) : Exception(string) -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomNumberGenerator.kt b/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomNumberGenerator.kt deleted file mode 100644 index 8ef490cc..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomNumberGenerator.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.pwgen - -import java.security.SecureRandom - -private val secureRandom = SecureRandom() - -/** 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() - -/** - * 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/dev/msfjarvis/aps/util/pwgen/RandomPasswordGenerator.kt b/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPasswordGenerator.kt deleted file mode 100644 index c1a8aeb1..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPasswordGenerator.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.pwgen - -import dev.msfjarvis.aps.util.extensions.hasFlag - -object RandomPasswordGenerator { - - /** - * Generates a random 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, 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. - */ - 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 password = "" - while (password.length < targetLength) { - val candidate = bank.secureRandomCharacter() - if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && - candidate in PasswordGenerator.AMBIGUOUS_STR - ) { - continue - } - password += candidate - } - return password.takeIf { PasswordGenerator.isValidPassword(it, pwFlags) } - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPhonemesGenerator.kt b/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPhonemesGenerator.kt deleted file mode 100644 index 5a5f5f21..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPhonemesGenerator.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.util.pwgen - -import dev.msfjarvis.aps.util.extensions.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.uppercase(Locale.ROOT) - val lowerCase = str.lowercase(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) } - } -} |