BIT-1419: Username generation options persistence (#586)

This commit is contained in:
Joshua Queen
2024-01-12 11:12:40 -05:00
committed by Álison Fernandes
parent 1f0a1bba6f
commit 5e2e23edec
9 changed files with 580 additions and 19 deletions

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
/**
* Primary access point for disk information related to generation.
@@ -16,4 +17,14 @@ interface GeneratorDiskSource {
* Stores a user's passcode generation options using a [userId].
*/
fun storePasscodeGenerationOptions(userId: String, options: PasscodeGenerationOptions?)
/**
* Retrieves a user's username generation options using a [userId].
*/
fun getUsernameGenerationOptions(userId: String): UsernameGenerationOptions?
/**
* Stores a user's username generation options using a [userId].
*/
fun storeUsernameGenerationOptions(userId: String, options: UsernameGenerationOptions?)
}

View File

@@ -3,10 +3,12 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val PASSWORD_GENERATION_OPTIONS_KEY = "passwordGenerationOptions"
private const val USERNAME_GENERATION_OPTIONS_KEY = "usernameGenerationOptions"
/**
* Primary implementation of [GeneratorDiskSource].
@@ -35,4 +37,20 @@ class GeneratorDiskSourceImpl(
private fun getPasswordGenerationOptionsKey(userId: String): String =
"${BASE_KEY}_${PASSWORD_GENERATION_OPTIONS_KEY}_$userId"
override fun getUsernameGenerationOptions(userId: String): UsernameGenerationOptions? {
val key = getUsernameGenerationOptionsKey(userId)
return getString(key)?.let { json.decodeFromString(it) }
}
override fun storeUsernameGenerationOptions(
userId: String,
options: UsernameGenerationOptions?,
) {
val key = getUsernameGenerationOptionsKey(userId)
putString(key, options?.let { json.encodeToString(it) })
}
private fun getUsernameGenerationOptionsKey(userId: String): String =
"${BASE_KEY}_${USERNAME_GENERATION_OPTIONS_KEY}_$userId"
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package com.x8bit.bitwarden.data.tools.generator.repository
import com.bitwarden.core.PassphraseGeneratorRequest
@@ -12,6 +14,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswo
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import kotlinx.coroutines.flow.StateFlow
/**
@@ -79,6 +82,16 @@ interface GeneratorRepository {
*/
fun savePasscodeGenerationOptions(options: PasscodeGenerationOptions)
/**
* Get the [UsernameGenerationOptions] for the current user.
*/
fun getUsernameGenerationOptions(): UsernameGenerationOptions?
/**
* Save the [UsernameGenerationOptions] for the current user.
*/
fun saveUsernameGenerationOptions(options: UsernameGenerationOptions)
/**
* Store a password history item for the current user.
*/

View File

@@ -22,6 +22,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswo
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -193,6 +194,16 @@ class GeneratorRepositoryImpl(
userId?.let { generatorDiskSource.storePasscodeGenerationOptions(it, options) }
}
override fun getUsernameGenerationOptions(): UsernameGenerationOptions? {
val userId = authDiskSource.userState?.activeUserId
return userId?.let { generatorDiskSource.getUsernameGenerationOptions(it) }
}
override fun saveUsernameGenerationOptions(options: UsernameGenerationOptions) {
val userId = authDiskSource.userState?.activeUserId
userId?.let { generatorDiskSource.storeUsernameGenerationOptions(it, options) }
}
override suspend fun storePasswordHistory(passwordHistoryView: PasswordHistoryView) {
val userId = authDiskSource.userState?.activeUserId ?: return
val encryptedPasswordHistory = vaultSdkSource

View File

@@ -0,0 +1,129 @@
package com.x8bit.bitwarden.data.tools.generator.repository.model
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* A data class representing the configuration options for generating usernames.
*
* @property type The type of username to be generated, as defined in UsernameType.
* @property serviceType The type of email forwarding service to be used,
* as defined in ForwardedEmailServiceType.
* @property capitalizeRandomWordUsername Indicates whether to capitalize the username.
* @property includeNumberRandomWordUsername Indicates whether to include a number in the username.
* @property plusAddressedEmail The email address to be used for plus-addressing.
* @property catchAllEmailDomain The domain name to be used for catch-all email addresses.
* @property firefoxRelayApiAccessToken The API access token for Firefox Relay.
* @property simpleLoginApiKey The API key for SimpleLogin.
* @property duckDuckGoApiKey The API key for DuckDuckGo.
* @property fastMailApiKey The API key for FastMail.
* @property anonAddyApiAccessToken The API access token for AnonAddy.
* @property anonAddyDomainName The domain name associated with AnonAddy.
* @property forwardEmailApiAccessToken The API access token for Forward Email.
* @property forwardEmailDomainName The domain name associated with Forward Email.
* @property emailWebsite The website associated with the email service.
*/
@Serializable
data class UsernameGenerationOptions(
@SerialName("type")
val type: UsernameType,
@SerialName("serviceType")
val serviceType: ForwardedEmailServiceType? = null,
@SerialName("capitalizeRandomWordUsername")
val capitalizeRandomWordUsername: Boolean? = null,
@SerialName("includeNumberRandomWordUsername")
val includeNumberRandomWordUsername: Boolean? = null,
@SerialName("plusAddressedEmail")
val plusAddressedEmail: String? = null,
@SerialName("catchAllEmailDomain")
val catchAllEmailDomain: String? = null,
@SerialName("firefoxRelayApiAccessToken")
val firefoxRelayApiAccessToken: String? = null,
@SerialName("simpleLoginApiKey")
val simpleLoginApiKey: String? = null,
@SerialName("duckDuckGoApiKey")
val duckDuckGoApiKey: String? = null,
@SerialName("fastMailApiKey")
val fastMailApiKey: String? = null,
@SerialName("anonAddyApiAccessToken")
val anonAddyApiAccessToken: String? = null,
@SerialName("anonAddyDomainName")
val anonAddyDomainName: String? = null,
@SerialName("forwardEmailApiAccessToken")
val forwardEmailApiAccessToken: String? = null,
@SerialName("forwardEmailDomainName")
val forwardEmailDomainName: String? = null,
@SerialName("emailWebsite")
val emailWebsite: String? = null,
) {
/**
* Represents different Username Types.
*/
@Serializable(with = UsernameTypeSerializer::class)
enum class UsernameType {
@SerialName("0")
PLUS_ADDRESSED_EMAIL,
@SerialName("1")
CATCH_ALL_EMAIL,
@SerialName("2")
FORWARDED_EMAIL_ALIAS,
@SerialName("3")
RANDOM_WORD,
}
/**
* Represents different Service Types within the ForwardedEmailAlias Username Type.
*/
@Serializable(with = ForwardedEmailServiceTypeSerializer::class)
enum class ForwardedEmailServiceType {
@SerialName("-1")
NONE,
@SerialName("0")
ANON_ADDY,
@SerialName("1")
FIREFOX_RELAY,
@SerialName("2")
SIMPLE_LOGIN,
@SerialName("3")
DUCK_DUCK_GO,
@SerialName("4")
FASTMAIL,
}
}
@Keep
private class UsernameTypeSerializer :
BaseEnumeratedIntSerializer<UsernameGenerationOptions.UsernameType>(
UsernameGenerationOptions.UsernameType.values(),
)
@Keep
private class ForwardedEmailServiceTypeSerializer :
BaseEnumeratedIntSerializer<UsernameGenerationOptions.ForwardedEmailServiceType>(
UsernameGenerationOptions.ForwardedEmailServiceType.values(),
)

View File

@@ -20,9 +20,11 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswo
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Passphrase
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passcode.PasscodeType.Password
@@ -140,7 +142,7 @@ class GeneratorViewModel @Inject constructor(
}
is GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult -> {
handleUpdateForwadedServiceGeneratedUsernameResult(action)
handleUpdateForwardedServiceGeneratedUsernameResult(action)
}
is GeneratorAction.MainType.Username.UsernameTypeOptionSelect -> {
@@ -233,21 +235,48 @@ class GeneratorViewModel @Inject constructor(
}
private fun loadUsernameOptions(selectedType: Username) {
val updatedSelectedType = when (selectedType.selectedType) {
is PlusAddressedEmail -> Username(
selectedType = PlusAddressedEmail(
// For convenience the default is an empty email value. We can supply the
// dynamic value here before updating the state.
email = state.currentEmailAddress,
),
)
val options = generatorRepository.getUsernameGenerationOptions()
val updatedSelectedType = when (val type = selectedType.selectedType) {
is PlusAddressedEmail -> {
val emailToUse = options
?.plusAddressedEmail
?.orNullIfBlank()
?: state.currentEmailAddress
else -> selectedType
Username(selectedType = PlusAddressedEmail(email = emailToUse))
}
is CatchAllEmail -> {
val catchAllEmail = CatchAllEmail(
domainName = options?.catchAllEmailDomain ?: type.domainName,
)
Username(selectedType = catchAllEmail)
}
is RandomWord -> {
val randomWord = RandomWord(
capitalize = options?.capitalizeRandomWordUsername ?: type.capitalize,
includeNumber = options?.includeNumberRandomWordUsername ?: type.includeNumber,
)
Username(selectedType = randomWord)
}
is ForwardedEmailAlias -> {
val mappedServiceType = options
?.serviceType
?.toServiceType(options)
?: type.selectedServiceType
Username(
selectedType = ForwardedEmailAlias(
selectedServiceType = mappedServiceType,
obfuscatedText = "",
),
)
}
}
mutableStateFlow.update {
it.copy(selectedType = updatedSelectedType)
}
mutableStateFlow.update { it.copy(selectedType = updatedSelectedType) }
}
private fun savePasswordOptionsToDisk(password: Password) {
@@ -278,6 +307,82 @@ class GeneratorViewModel @Inject constructor(
generatorRepository.savePasscodeGenerationOptions(newOptions)
}
private fun savePlusAddressedEmailOptionsToDisk(plusAddressedEmail: PlusAddressedEmail) {
val options = generatorRepository.getUsernameGenerationOptions()
?: generateUsernameDefaultOptions()
val newOptions = options.copy(
type = UsernameGenerationOptions.UsernameType.PLUS_ADDRESSED_EMAIL,
plusAddressedEmail = plusAddressedEmail.email,
)
generatorRepository.saveUsernameGenerationOptions(newOptions)
}
private fun saveCatchAllEmailOptionsToDisk(catchAllEmail: CatchAllEmail) {
val options = generatorRepository
.getUsernameGenerationOptions() ?: generateUsernameDefaultOptions()
val newOptions = options.copy(
type = UsernameGenerationOptions.UsernameType.CATCH_ALL_EMAIL,
catchAllEmailDomain = catchAllEmail.domainName,
)
generatorRepository.saveUsernameGenerationOptions(newOptions)
}
private fun saveRandomWordOptionsToDisk(randomWord: RandomWord) {
val options = generatorRepository
.getUsernameGenerationOptions() ?: generateUsernameDefaultOptions()
val newOptions = options.copy(
type = UsernameGenerationOptions.UsernameType.RANDOM_WORD,
capitalizeRandomWordUsername = randomWord.capitalize,
includeNumberRandomWordUsername = randomWord.includeNumber,
)
generatorRepository.saveUsernameGenerationOptions(newOptions)
}
private fun saveForwardedEmailAliasServiceTypeToDisk(forwardedEmailAlias: ForwardedEmailAlias) {
val options =
generatorRepository.getUsernameGenerationOptions() ?: generateUsernameDefaultOptions()
val newOptions = when (forwardedEmailAlias.selectedServiceType) {
is AddyIo -> options.copy(
type = UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.ANON_ADDY,
anonAddyApiAccessToken = forwardedEmailAlias.selectedServiceType.apiAccessToken,
anonAddyDomainName = forwardedEmailAlias.selectedServiceType.domainName,
)
is DuckDuckGo -> options.copy(
type = UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.DUCK_DUCK_GO,
duckDuckGoApiKey = forwardedEmailAlias.selectedServiceType.apiKey,
)
is FastMail -> options.copy(
type = UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.FASTMAIL,
fastMailApiKey = forwardedEmailAlias.selectedServiceType.apiKey,
)
is FirefoxRelay -> options.copy(
type = UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.FIREFOX_RELAY,
firefoxRelayApiAccessToken = forwardedEmailAlias.selectedServiceType.apiAccessToken,
)
is SimpleLogin -> options.copy(
type = UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.SIMPLE_LOGIN,
simpleLoginApiKey = forwardedEmailAlias.selectedServiceType.apiKey,
)
else -> options.copy(
type = UsernameGenerationOptions.UsernameType.FORWARDED_EMAIL_ALIAS,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.NONE,
)
}
generatorRepository.saveUsernameGenerationOptions(newOptions)
}
private fun generatePasscodeDefaultOptions(): PasscodeGenerationOptions {
val defaultPassword = Password()
val defaultPassphrase = Passphrase()
@@ -298,6 +403,26 @@ class GeneratorViewModel @Inject constructor(
)
}
private fun generateUsernameDefaultOptions(): UsernameGenerationOptions {
return UsernameGenerationOptions(
type = UsernameGenerationOptions.UsernameType.PLUS_ADDRESSED_EMAIL,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.NONE,
capitalizeRandomWordUsername = false,
includeNumberRandomWordUsername = false,
plusAddressedEmail = "",
catchAllEmailDomain = "",
firefoxRelayApiAccessToken = "",
simpleLoginApiKey = "",
duckDuckGoApiKey = "",
fastMailApiKey = "",
anonAddyApiAccessToken = "",
anonAddyDomainName = "",
forwardEmailApiAccessToken = "",
forwardEmailDomainName = "",
emailWebsite = "",
)
}
private suspend fun generatePassword(password: Password) {
val request = PasswordGeneratorRequest(
lowercase = password.useLowercase,
@@ -423,7 +548,7 @@ class GeneratorViewModel @Inject constructor(
}
}
private fun handleUpdateForwadedServiceGeneratedUsernameResult(
private fun handleUpdateForwardedServiceGeneratedUsernameResult(
action: GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult,
) {
when (val result = action.result) {
@@ -702,25 +827,48 @@ class GeneratorViewModel @Inject constructor(
.ForwardedEmailAlias
.ServiceTypeOptionSelect,
) {
val options = generatorRepository.getUsernameGenerationOptions()
?: generateUsernameDefaultOptions()
when (action.serviceTypeOption) {
ForwardedEmailAlias.ServiceTypeOption.ADDY_IO -> updateForwardedEmailAliasType {
ForwardedEmailAlias(selectedServiceType = AddyIo())
ForwardedEmailAlias(
selectedServiceType = AddyIo(
apiAccessToken = options.anonAddyApiAccessToken.orEmpty(),
domainName = options.anonAddyDomainName.orEmpty(),
),
)
}
ForwardedEmailAlias.ServiceTypeOption.DUCK_DUCK_GO -> updateForwardedEmailAliasType {
ForwardedEmailAlias(selectedServiceType = DuckDuckGo())
ForwardedEmailAlias(
selectedServiceType = DuckDuckGo(
apiKey = options.duckDuckGoApiKey.orEmpty(),
),
)
}
ForwardedEmailAlias.ServiceTypeOption.FAST_MAIL -> updateForwardedEmailAliasType {
ForwardedEmailAlias(selectedServiceType = FastMail())
ForwardedEmailAlias(
selectedServiceType = FastMail(
apiKey = options.fastMailApiKey.orEmpty(),
),
)
}
ForwardedEmailAlias.ServiceTypeOption.FIREFOX_RELAY -> updateForwardedEmailAliasType {
ForwardedEmailAlias(selectedServiceType = FirefoxRelay())
ForwardedEmailAlias(
selectedServiceType = FirefoxRelay(
apiAccessToken = options.firefoxRelayApiAccessToken.orEmpty(),
),
)
}
ForwardedEmailAlias.ServiceTypeOption.SIMPLE_LOGIN -> updateForwardedEmailAliasType {
ForwardedEmailAlias(selectedServiceType = SimpleLogin())
ForwardedEmailAlias(
selectedServiceType = SimpleLogin(
apiKey = options.simpleLoginApiKey.orEmpty(),
),
)
}
}
}
@@ -962,24 +1110,28 @@ class GeneratorViewModel @Inject constructor(
is Username -> when (val selectedType = updatedMainType.selectedType) {
is ForwardedEmailAlias -> {
saveForwardedEmailAliasServiceTypeToDisk(selectedType)
if (isManualRegeneration) {
generateForwardedEmailAlias(selectedType)
}
}
is CatchAllEmail -> {
saveCatchAllEmailOptionsToDisk(selectedType)
if (isManualRegeneration) {
generateCatchAllEmail(selectedType)
}
}
is PlusAddressedEmail -> {
savePlusAddressedEmailOptionsToDisk(selectedType)
if (isManualRegeneration) {
generatePlusAddressedEmail(selectedType)
}
}
is RandomWord -> {
saveRandomWordOptionsToDisk(selectedType)
if (isManualRegeneration) {
generateRandomWordUsername(selectedType)
}
@@ -1024,6 +1176,7 @@ class GeneratorViewModel @Inject constructor(
)
sendAction(GeneratorAction.Internal.UpdateGeneratedRandomWordUsernameResult(result))
}
private inline fun updateGeneratorMainTypePasscode(
crossinline block: (Passcode) -> Passcode,
) {
@@ -2050,3 +2203,34 @@ private fun Password.enforceAtLeastOneToggleOn(): Password =
} else {
this
}
private fun UsernameGenerationOptions.ForwardedEmailServiceType?.toServiceType(
options: UsernameGenerationOptions,
): ForwardedEmailAlias.ServiceType? {
return when (this) {
UsernameGenerationOptions.ForwardedEmailServiceType.FIREFOX_RELAY -> {
FirefoxRelay(apiAccessToken = options.firefoxRelayApiAccessToken.orEmpty())
}
UsernameGenerationOptions.ForwardedEmailServiceType.SIMPLE_LOGIN -> {
SimpleLogin(apiKey = options.simpleLoginApiKey.orEmpty())
}
UsernameGenerationOptions.ForwardedEmailServiceType.DUCK_DUCK_GO -> {
DuckDuckGo(apiKey = options.duckDuckGoApiKey.orEmpty())
}
UsernameGenerationOptions.ForwardedEmailServiceType.FASTMAIL -> {
FastMail(apiKey = options.fastMailApiKey.orEmpty())
}
UsernameGenerationOptions.ForwardedEmailServiceType.ANON_ADDY -> {
AddyIo(
apiAccessToken = options.anonAddyApiAccessToken.orEmpty(),
domainName = options.anonAddyDomainName.orEmpty(),
)
}
else -> null
}
}