Refactor randomized password generator into a separate module (#1663)
This commit is contained in:
parent
c1ef2e7341
commit
abc62c2b6b
14 changed files with 126 additions and 92 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -15,5 +15,6 @@ apiValidation {
|
|||
"crypto-pgpainless",
|
||||
"format-common",
|
||||
"diceware",
|
||||
"random",
|
||||
)
|
||||
}
|
||||
|
|
1
passgen/random/.gitignore
vendored
Normal file
1
passgen/random/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
4
passgen/random/build.gradle.kts
Normal file
4
passgen/random/build.gradle.kts
Normal file
|
@ -0,0 +1,4 @@
|
|||
plugins {
|
||||
kotlin("jvm")
|
||||
id("com.github.android-password-store.kotlin-library")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
|
@ -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")
|
||||
}
|
|
@ -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)]
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -53,3 +53,5 @@ include("format-common")
|
|||
include("openpgp-ktx")
|
||||
|
||||
include("passgen:diceware")
|
||||
|
||||
include(":passgen:random")
|
||||
|
|
Loading…
Reference in a new issue