diff options
14 files changed, 126 insertions, 92 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 443121cf..e171a814 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) 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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b476587..c11e6f25 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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> diff --git a/build-logic/kotlin-plugins/src/main/kotlin/com.github.android-password-store.binary-compatibility.gradle.kts b/build-logic/kotlin-plugins/src/main/kotlin/com.github.android-password-store.binary-compatibility.gradle.kts index 4e0ceaa3..2aec2844 100644 --- a/build-logic/kotlin-plugins/src/main/kotlin/com.github.android-password-store.binary-compatibility.gradle.kts +++ b/build-logic/kotlin-plugins/src/main/kotlin/com.github.android-password-store.binary-compatibility.gradle.kts @@ -15,5 +15,6 @@ apiValidation { "crypto-pgpainless", "format-common", "diceware", + "random", ) } diff --git a/passgen/random/.gitignore b/passgen/random/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/passgen/random/.gitignore @@ -0,0 +1 @@ +/build
\ No newline at end of file diff --git a/passgen/random/build.gradle.kts b/passgen/random/build.gradle.kts new file mode 100644 index 00000000..f91458dd --- /dev/null +++ b/passgen/random/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + kotlin("jvm") + id("com.github.android-password-store.kotlin-library") +} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/PasswordGenerator.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordGenerator.kt index bd21ea0a..fcd28311 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/PasswordGenerator.kt +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordGenerator.kt @@ -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") -} - -object PasswordGenerator { +public object PasswordGenerator { - const val DEFAULT_LENGTH = 16 + public const val DEFAULT_LENGTH: Int = 16 - 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 = 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_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 - } + 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" - 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) } diff --git a/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordGeneratorException.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordGeneratorException.kt new file mode 100644 index 00000000..b7d70c39 --- /dev/null +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordGeneratorException.kt @@ -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() diff --git a/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordOption.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordOption.kt new file mode 100644 index 00000000..70d0da22 --- /dev/null +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/PasswordOption.kt @@ -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") +} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomNumberGenerator.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/RandomNumberGenerator.kt index 8ef490cc..3bb34325 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomNumberGenerator.kt +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/RandomNumberGenerator.kt @@ -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)] diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPasswordGenerator.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/RandomPasswordGenerator.kt index c1a8aeb1..32d3ff49 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPasswordGenerator.kt +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/RandomPasswordGenerator.kt @@ -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] diff --git a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPhonemesGenerator.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/RandomPhonemesGenerator.kt index 5a5f5f21..b9df4324 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/pwgen/RandomPhonemesGenerator.kt +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/RandomPhonemesGenerator.kt @@ -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 diff --git a/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/util/Extensions.kt b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/util/Extensions.kt new file mode 100644 index 00000000..3b38e22b --- /dev/null +++ b/passgen/random/src/main/kotlin/dev/msfjarvis/aps/passgen/random/util/Extensions.kt @@ -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 +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bc0c5ab3..3b2f4d2a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -53,3 +53,5 @@ include("format-common") include("openpgp-ktx") include("passgen:diceware") + +include(":passgen:random") |