Refactor randomized password generator into a separate module (#1663)

This commit is contained in:
Aditya Wasan 2022-01-13 22:13:53 +05:30 committed by GitHub
parent c1ef2e7341
commit abc62c2b6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 126 additions and 92 deletions

View file

@ -79,6 +79,7 @@ dependencies {
implementation(projects.formatCommon)
implementation(projects.openpgpKtx)
implementation(projects.passgen.diceware)
implementation(projects.passgen.random)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.autofill)

View file

@ -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,9 +123,8 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
return requireDialog().findViewById<CheckBox>(id).isChecked
}
private fun setPreferences() {
val preferences =
listOfNotNull(
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) },
@ -118,8 +132,25 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
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
}
}

View file

@ -391,5 +391,6 @@
<string name="pgp_key_import_succeeded">Successfully imported PGP key</string>
<string name="pgp_key_import_succeeded_message">The key ID of the imported key is given below, please review it for correctness:\n%1$s</string>
<string name="pref_category_pgp_title">PGP settings</string>
<string name="pwgen_some_error_occurred">Some error occurred</string>
</resources>

View file

@ -15,5 +15,6 @@ apiValidation {
"crypto-pgpainless",
"format-common",
"diceware",
"random",
)
}

1
passgen/random/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,4 @@
plugins {
kotlin("jvm")
id("com.github.android-password-store.kotlin-library")
}

View file

@ -2,56 +2,28 @@
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package dev.msfjarvis.aps.util.pwgen
package dev.msfjarvis.aps.passgen.random
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
import dev.msfjarvis.aps.passgen.random.util.clearFlag
import dev.msfjarvis.aps.passgen.random.util.hasFlag
enum class PasswordOption(val key: String) {
NoDigits("0"),
NoUppercaseLetters("A"),
NoAmbiguousCharacters("B"),
FullyRandom("s"),
AtLeastOneSymbol("y"),
NoLowercaseLetters("L")
}
public object PasswordGenerator {
object PasswordGenerator {
public const val DEFAULT_LENGTH: Int = 16
const val DEFAULT_LENGTH = 16
internal const val DIGITS = 0x0001
internal const val UPPERS = 0x0002
internal const val SYMBOLS = 0x0004
internal const val NO_AMBIGUOUS = 0x0008
internal const val LOWERS = 0x0020
const val DIGITS = 0x0001
const val UPPERS = 0x0002
const val SYMBOLS = 0x0004
const val NO_AMBIGUOUS = 0x0008
const val LOWERS = 0x0020
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"
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 {
internal 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
@ -60,17 +32,15 @@ object PasswordGenerator {
return true
}
/** Generates a password using the preferences set by [setPrefs]. */
/** Generates a password using the given [passwordOptions] and [length]. */
@Throws(PasswordGeneratorException::class)
fun generate(ctx: Context): String {
val prefs = ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
public fun generate(passwordOptions: List<PasswordOption>, length: Int = DEFAULT_LENGTH): String {
var numCharacterCategories = 0
var phonemes = true
var pwgenFlags = DIGITS or UPPERS or LOWERS
for (option in PasswordOption.values()) {
if (prefs.getBoolean(option.key, false)) {
if (option in passwordOptions) {
when (option) {
PasswordOption.NoDigits -> pwgenFlags = pwgenFlags.clearFlag(DIGITS)
PasswordOption.NoUppercaseLetters -> pwgenFlags = pwgenFlags.clearFlag(UPPERS)
@ -98,14 +68,11 @@ object PasswordGenerator {
}
}
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))
throw NoCharactersIncludedException()
}
if (length < numCharacterCategories) {
throw PasswordGeneratorException(
ctx.resources.getString(R.string.pwgen_length_too_short_error)
)
throw PasswordLengthTooShortException()
}
if (!(pwgenFlags hasFlag UPPERS) && !(pwgenFlags hasFlag LOWERS)) {
phonemes = false
@ -120,10 +87,7 @@ object PasswordGenerator {
var password: String?
var iterations = 0
do {
if (iterations++ > 1000)
throw PasswordGeneratorException(
ctx.resources.getString(R.string.pwgen_max_iterations_exceeded)
)
if (iterations++ > 1000) throw MaxIterationsExceededException()
password =
if (phonemes) {
RandomPhonemesGenerator.generate(length, pwgenFlags)
@ -133,6 +97,4 @@ object PasswordGenerator {
} while (password == null)
return password
}
class PasswordGeneratorException(string: String) : Exception(string)
}

View file

@ -0,0 +1,10 @@
package dev.msfjarvis.aps.passgen.random
public sealed class PasswordGeneratorException(message: String? = null, cause: Throwable? = null) :
Throwable(message, cause)
public class MaxIterationsExceededException : PasswordGeneratorException()
public class NoCharactersIncludedException : PasswordGeneratorException()
public class PasswordLengthTooShortException : PasswordGeneratorException()

View file

@ -0,0 +1,10 @@
package dev.msfjarvis.aps.passgen.random
public enum class PasswordOption(public val key: String) {
NoDigits("0"),
NoUppercaseLetters("A"),
NoAmbiguousCharacters("B"),
FullyRandom("s"),
AtLeastOneSymbol("y"),
NoLowercaseLetters("L")
}

View file

@ -2,30 +2,30 @@
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package dev.msfjarvis.aps.util.pwgen
package dev.msfjarvis.aps.passgen.random
import java.security.SecureRandom
private val secureRandom = SecureRandom()
/** Returns a number between 0 (inclusive) and [exclusiveBound](exclusive). */
fun secureRandomNumber(exclusiveBound: Int) = secureRandom.nextInt(exclusiveBound)
internal fun secureRandomNumber(exclusiveBound: Int) = secureRandom.nextInt(exclusiveBound)
/** Returns `true` and `false` with probablity 50% each. */
fun secureRandomBoolean() = secureRandom.nextBoolean()
internal fun secureRandomBoolean() = secureRandom.nextBoolean()
/**
* Returns `true` with probability [percentTrue]% and `false` with probability `(100 - [percentTrue]
* )`%.
*/
fun secureRandomBiasedBoolean(percentTrue: Int): Boolean {
internal 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)]
internal fun <T> Array<T>.secureRandomElement() = this[secureRandomNumber(size)]
fun <T> List<T>.secureRandomElement() = this[secureRandomNumber(size)]
internal fun <T> List<T>.secureRandomElement() = this[secureRandomNumber(size)]
fun String.secureRandomCharacter() = this[secureRandomNumber(length)]
internal fun String.secureRandomCharacter() = this[secureRandomNumber(length)]

View file

@ -2,11 +2,11 @@
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package dev.msfjarvis.aps.util.pwgen
package dev.msfjarvis.aps.passgen.random
import dev.msfjarvis.aps.util.extensions.hasFlag
import dev.msfjarvis.aps.passgen.random.util.hasFlag
object RandomPasswordGenerator {
internal object RandomPasswordGenerator {
/**
* Generates a random password of length [targetLength], taking the following flags in [pwFlags]

View file

@ -2,12 +2,12 @@
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package dev.msfjarvis.aps.util.pwgen
package dev.msfjarvis.aps.passgen.random
import dev.msfjarvis.aps.util.extensions.hasFlag
import dev.msfjarvis.aps.passgen.random.util.hasFlag
import java.util.Locale
object RandomPhonemesGenerator {
internal object RandomPhonemesGenerator {
private const val CONSONANT = 0x0001
private const val VOWEL = 0x0002

View file

@ -0,0 +1,11 @@
package dev.msfjarvis.aps.passgen.random.util
/** Clears the given [flag] from the value of this [Int] */
internal infix fun Int.clearFlag(flag: Int): Int {
return this and flag.inv()
}
/** Checks if this [Int] contains the given [flag] */
internal infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
}

View file

@ -53,3 +53,5 @@ include("format-common")
include("openpgp-ktx")
include("passgen:diceware")
include(":passgen:random")