Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import android.widget.*
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.password.PassphraseGenerator
Expand All @@ -46,10 +47,15 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
private lateinit var charactersCountText: TextView
private lateinit var wordSeparator: EditText
private lateinit var wordCaseSpinner: Spinner
private lateinit var separatorTypeSpinner: Spinner
private lateinit var wordSeparatorLayout: TextInputLayout
private lateinit var randomDigitsLayout: TextInputLayout
private lateinit var randomDigitsCount: EditText

private var minSliderWordCount: Int = 0
private var maxSliderWordCount: Int = 0
private var wordCaseAdapter: ArrayAdapter<String>? = null
private var separatorTypeAdapter: ArrayAdapter<String>? = null

private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()

Expand All @@ -69,6 +75,10 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
charactersCountText = view.findViewById(R.id.character_count)
wordSeparator = view.findViewById(R.id.word_separator)
wordCaseSpinner = view.findViewById(R.id.word_case)
separatorTypeSpinner = view.findViewById(R.id.separator_type)
wordSeparatorLayout = view.findViewById(R.id.word_separator_layout)
randomDigitsLayout = view.findViewById(R.id.random_digits_layout)
randomDigitsCount = view.findViewById(R.id.random_digits_count)

minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
Expand All @@ -90,6 +100,12 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
wordCaseSpinner.adapter = wordCaseAdapter

separatorTypeAdapter = ArrayAdapter(context,
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.separator_type_array)).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
separatorTypeSpinner.adapter = separatorTypeAdapter
}

loadSettings()
Expand Down Expand Up @@ -142,6 +158,17 @@ class PassphraseGeneratorFragment : DatabaseFragment() {

override fun onNothingSelected(parent: AdapterView<*>?) {}
}
separatorTypeSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
updateSeparatorTypeVisibility()
generatePassphrase()
}

override fun onNothingSelected(parent: AdapterView<*>?) {}
}
randomDigitsCount.doOnTextChanged { _, _, _, _ ->
generatePassphrase()
}

generatePassphrase()

Expand Down Expand Up @@ -209,13 +236,50 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
wordSeparator.setText(separator)
}

private fun getSeparatorType(): PassphraseGenerator.SeparatorType {
return when (separatorTypeSpinner.selectedItemPosition) {
1 -> PassphraseGenerator.SeparatorType.RANDOM_NUMBERS
else -> PassphraseGenerator.SeparatorType.CUSTOM_VALUE
}
}

private fun setSeparatorType(separatorType: PassphraseGenerator.SeparatorType) {
separatorTypeSpinner.setSelection(if (separatorType == PassphraseGenerator.SeparatorType.RANDOM_NUMBERS) 1 else 0)
}

private fun getRandomDigitsCount(): Int {
return try {
val text = randomDigitsCount.text.toString()
if (text.isEmpty()) 1 else Integer.valueOf(text).coerceIn(1, 9)
} catch (numberException: NumberFormatException) {
1
}
}

private fun setRandomDigitsCount(count: Int) {
randomDigitsCount.setText(count.toString())
}

private fun updateSeparatorTypeVisibility() {
val separatorType = getSeparatorType()
if (separatorType == PassphraseGenerator.SeparatorType.RANDOM_NUMBERS) {
randomDigitsLayout.visibility = View.VISIBLE
wordSeparatorLayout.visibility = View.GONE
} else {
randomDigitsLayout.visibility = View.GONE
wordSeparatorLayout.visibility = View.VISIBLE
}
}

private fun generatePassphrase() {
var passphrase = ""
try {
passphrase = PassphraseGenerator().generatePassphrase(
getWordCount(),
getWordSeparator(),
getWordCase())
getWordCase(),
getSeparatorType(),
getRandomDigitsCount())
} catch (e: Exception) {
Log.e(TAG, "Unable to generate a passphrase", e)
}
Expand All @@ -233,6 +297,8 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
PreferencesUtil.setDefaultPassphraseSeparatorType(context, getSeparatorType().toPreferenceString())
PreferencesUtil.setDefaultPassphraseRandomDigitsCount(context, getRandomDigitsCount())
}
}

Expand All @@ -241,6 +307,9 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
setSeparatorType(PassphraseGenerator.SeparatorType.fromString(PreferencesUtil.getDefaultPassphraseSeparatorType(context)))
setRandomDigitsCount(PreferencesUtil.getDefaultPassphraseRandomDigitsCount(context))
updateSeparatorTypeVisibility()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,40 @@
package com.kunzisoft.keepass.password

import me.gosimple.nbvcxz.resources.Generator
import java.security.SecureRandom

class PassphraseGenerator {

@Throws(IllegalArgumentException::class)
fun generatePassphrase(wordCount: Int,
wordSeparator: String,
wordCase: WordCase): String {
wordCase: WordCase,
separatorType: SeparatorType,
randomDigitsCount: Int): String {
val effectiveSeparator = if (separatorType == SeparatorType.RANDOM_NUMBERS) TEMP_SPLIT else wordSeparator

// From eff_large dictionary
return when (wordCase) {
val passphrase = when (wordCase) {
WordCase.LOWER_CASE -> {
Generator.generatePassphrase(wordSeparator, wordCount)
Generator.generatePassphrase(effectiveSeparator, wordCount)
}
WordCase.UPPER_CASE -> {
applyWordCase(wordCount, wordSeparator) { word ->
applyWordCase(wordCount, effectiveSeparator) { word ->
word.uppercase()
}
}
WordCase.TITLE_CASE -> {
applyWordCase(wordCount, wordSeparator) { word ->
applyWordCase(wordCount, effectiveSeparator) { word ->
word.replaceFirstChar { char -> char.uppercaseChar() }
}
}
}

return if (separatorType == SeparatorType.RANDOM_NUMBERS) {
replaceSeparatorWithRandomNumbers(passphrase, effectiveSeparator, randomDigitsCount)
} else {
passphrase
}
}

private fun applyWordCase(wordCount: Int,
Expand All @@ -58,6 +69,26 @@ class PassphraseGenerator {
return stringBuilder.toString().removeSuffix(wordSeparator)
}

private fun replaceSeparatorWithRandomNumbers(passphrase: String,
separator: String,
digitsCount: Int): String {
val random = SecureRandom()
val splitWords = passphrase.split(separator)
val stringBuilder = StringBuilder()

splitWords.forEachIndexed { index, word ->
stringBuilder.append(word)
if (index < splitWords.size - 1) {
repeat(digitsCount) {
// Use only digits 1-9 to avoid ambiguity between 0 and O
stringBuilder.append(random.nextInt(9) + 1)A
}
}
}

return stringBuilder.toString()
}

enum class WordCase {
LOWER_CASE,
UPPER_CASE,
Expand All @@ -74,6 +105,27 @@ class PassphraseGenerator {
}
}

enum class SeparatorType {
CUSTOM_VALUE,
RANDOM_NUMBERS;

fun toPreferenceString(): String {
return when (this) {
RANDOM_NUMBERS -> "random_numbers"
CUSTOM_VALUE -> "custom_value"
}
}

companion object {
fun fromString(value: String): SeparatorType {
return when (value) {
"random_numbers" -> RANDOM_NUMBERS
else -> CUSTOM_VALUE
}
}
}
}

companion object {
private const val TEMP_SPLIT = "-"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,38 @@ object PreferencesUtil {
}
}

fun getDefaultPassphraseSeparatorType(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getString(context.getString(R.string.passphrase_generator_separator_type_key),
context.getString(R.string.passphrase_generator_separator_type_default)) ?: "custom_value"
}

fun setDefaultPassphraseSeparatorType(context: Context, separatorType: String) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putString(
context.getString(R.string.passphrase_generator_separator_type_key),
separatorType
)
apply()
}
}

fun getDefaultPassphraseRandomDigitsCount(context: Context): Int {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getInt(context.getString(R.string.passphrase_generator_random_digits_count_key),
context.resources.getInteger(R.integer.passphrase_generator_random_digits_count_default))
}

fun setDefaultPassphraseRandomDigitsCount(context: Context, digitsCount: Int) {
PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
putInt(
context.getString(R.string.passphrase_generator_random_digits_count_key),
digitsCount
)
apply()
}
}

fun getDefaultSearchParameters(context: Context): SearchParameters {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return SearchParameters().apply {
Expand Down Expand Up @@ -866,6 +898,8 @@ object PreferencesUtil {
context.getString(R.string.passphrase_generator_word_count_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.passphrase_generator_word_case_key) -> editor.putInt(name, value.toInt())
context.getString(R.string.passphrase_generator_separator_key) -> editor.putString(name, value)
context.getString(R.string.passphrase_generator_separator_type_key) -> editor.putString(name, value)
context.getString(R.string.passphrase_generator_random_digits_count_key) -> editor.putInt(name, value.toInt())

context.getString(R.string.sort_node_key) -> editor.putString(name, value)
context.getString(R.string.sort_group_before_key) -> editor.putBoolean(name, value.toBoolean())
Expand Down
54 changes: 44 additions & 10 deletions app/src/main/res/layout/fragment_generate_passphrase.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,30 +132,64 @@
android:layout_height="wrap_content"
tools:text="Character count: 51" />

<RelativeLayout
<Spinner
android:id="@+id/word_case"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="48dp"
android:layout_marginTop="12dp"/>

<Spinner
android:id="@+id/word_case"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="12dp"
android:baselineAligned="false">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Separator"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"/>

<Spinner
android:id="@+id/separator_type"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginTop="12dp"/>
android:layout_weight="1"/>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/word_separator_layout"
android:layout_width="match_parent"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/word_case"
android:layout_toRightOf="@+id/word_case">
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/word_separator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/word_separator" />
android:hint="@string/value" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/random_digits_layout"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:visibility="gone">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/random_digits_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/length"
android:inputType="number"
android:maxLength="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

</LinearLayout>
</FrameLayout>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@
<string name="passphrase_generator_word_case_key" translatable="false">passphrase_generator_word_case_key</string>
<string name="passphrase_generator_separator_key" translatable="false">passphrase_generator_separator_key</string>
<string name="passphrase_generator_separator_default" translatable="false" />
<string name="passphrase_generator_separator_type_key" translatable="false">passphrase_generator_separator_type_key</string>
<string name="passphrase_generator_separator_type_default" translatable="false">custom_value</string>
<string name="passphrase_generator_random_digits_count_key" translatable="false">passphrase_generator_random_digits_count_key</string>
<integer name="passphrase_generator_random_digits_count_default" translatable="false">1</integer>

<!-- Search settings -->
<string name="search_option_case_sensitive_key" translatable="false">search_option_case_sensitive_key</string>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
<string name="invalid_db_sig">Could not recognize the database format.</string>
<string name="keyfile_is_empty">The keyfile is empty.</string>
<string name="length">Length</string>
<string name="value">Value</string>
<string name="nodes">Nodes</string>
<string name="hide_password_title">Hide passwords</string>
<string name="hide_password_summary">Mask passwords (***) by default</string>
Expand Down Expand Up @@ -654,6 +655,8 @@
<string name="exclude_ambiguous_chars">Exclude ambiguous characters</string>
<string name="consider_chars_filter">Consider characters</string>
<string name="word_separator">Separator</string>
<string name="separator_custom_value">Custom Value</string>
<string name="separator_random_numbers">Random Numbers</string>
<string name="ignore_chars_filter">Ignore characters</string>
<string name="lower_case">lower case</string>
<string name="upper_case">UPPER CASE</string>
Expand All @@ -665,6 +668,10 @@
<item>@string/upper_case</item>
<item>@string/title_case</item>
</string-array>
<string-array name="separator_type_array">
<item>@string/separator_custom_value</item>
<item>@string/separator_random_numbers</item>
</string-array>
<string-array name="timeout_options">
<item>5 seconds</item>
<item>10 seconds</item>
Expand Down