Make issuer required and account name optional (#55)

This commit is contained in:
Patrick Honkonen
2024-04-25 17:17:15 -04:00
committed by GitHub
parent cb158b0947
commit 1f0881f74e
18 changed files with 125 additions and 90 deletions

View File

@@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "8724b95439edde85bd15e0bd2e02195e",
"identityHash": "480a4540e7704429515a28eb9c38ab14",
"entities": [
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `key` TEXT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `algorithm` TEXT NOT NULL, `period` INTEGER NOT NULL, `digits` INTEGER NOT NULL, `issuer` TEXT, `userId` TEXT, PRIMARY KEY(`id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `key` TEXT NOT NULL, `type` TEXT NOT NULL, `algorithm` TEXT NOT NULL, `period` INTEGER NOT NULL, `digits` INTEGER NOT NULL, `issuer` TEXT NOT NULL, `userId` TEXT, `accountName` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@@ -20,12 +20,6 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
@@ -54,13 +48,19 @@
"fieldPath": "issuer",
"columnName": "issuer",
"affinity": "TEXT",
"notNull": false
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
@@ -76,7 +76,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8724b95439edde85bd15e0bd2e02195e')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '480a4540e7704429515a28eb9c38ab14')"
]
}
}

View File

@@ -1,5 +1,7 @@
package com.bitwarden.authenticator.data.authenticator.datasource.disk.entity
import android.net.Uri
import androidx.core.text.htmlEncode
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@@ -16,9 +18,6 @@ data class AuthenticatorItemEntity(
@ColumnInfo(name = "key")
val key: String,
@ColumnInfo(name = "accountName")
val accountName: String,
@ColumnInfo(name = "type")
val type: AuthenticatorItemType = AuthenticatorItemType.TOTP,
@@ -32,8 +31,42 @@ data class AuthenticatorItemEntity(
val digits: Int = 6,
@ColumnInfo(name = "issuer")
val issuer: String? = null,
val issuer: String,
@ColumnInfo(name = "userId")
val userId: String? = null,
)
@ColumnInfo(name = "accountName")
val accountName: String? = null,
) {
fun toOtpAuthUriString(): String {
return when (type) {
AuthenticatorItemType.TOTP -> {
val label = if (accountName.isNullOrBlank()) {
issuer
} else {
"$issuer:$accountName"
}
Uri.Builder()
.scheme("otpauth")
.authority("totp")
.appendPath(label.htmlEncode())
.appendQueryParameter("secret", key)
.appendQueryParameter("algorithm", algorithm.name)
.appendQueryParameter("digits", digits.toString())
.appendQueryParameter("period", period.toString())
.appendQueryParameter("issuer", issuer)
.build()
.toString()
}
AuthenticatorItemType.STEAM -> {
if (key.startsWith("steam://")) {
key
} else {
"steam://$key"
}
}
}
}
}

View File

@@ -75,7 +75,7 @@ class TotpCodeManagerImpl @Inject constructor(
return mutableVerificationCodeStateFlowMap.getOrPut(itemEntity) {
flow<DataState<VerificationCodeItem?>> {
val totpCode = itemEntity.key
val totpCode = itemEntity.toOtpAuthUriString()
var item: VerificationCodeItem? = null
while (currentCoroutineContext().isActive) {

View File

@@ -36,16 +36,10 @@ data class ExportJsonData(
* This model is loosely based off of Bitwarden's Cipher.Login JSON.
*
* @property totp OTP secret used to generate a verification code.
* @property issuer Optional issuer of the 2fa code.
* @property period Optional refresh period in seconds. Default is 30.
* @property digits Optional number of digits in the verification code. Default is 6
*/
@Serializable
data class ItemLoginData(
val totp: String,
val issuer: String?,
val period: Int,
val digits: Int,
)
}
}

View File

@@ -22,7 +22,7 @@ data class VerificationCodeItem(
val issuer: String?,
) {
/**
* The composite label of the authenticator item.
* The composite label of the authenticator item. Used for constructing an OTPAuth URI.
* ```
* label = issuer (“:” / “%3A”) *”%20” username
* ```

View File

@@ -233,7 +233,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult {
val headerLine =
"folder,favorite,type,name,login_uri,login_totp,issuer,period,digits"
"folder,favorite,type,name,login_uri,login_totp"
val dataLines = authenticatorDiskSource
.getItems()
.firstOrNull()
@@ -250,7 +250,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
private fun AuthenticatorItemEntity.toCsvFormat() =
",,1,$accountName,,$key,$issuer,$period,$digits"
",,1,$issuer,,${toOtpAuthUriString()},$issuer,$period,$digits"
private suspend fun encodeVaultDataToJson(fileUri: Uri): ExportDataResult {
val dataString: String = Json.encodeToString(
@@ -281,14 +281,11 @@ class AuthenticatorRepositoryImpl @Inject constructor(
folderId = null,
organizationId = null,
collectionIds = null,
name = accountName,
name = issuer,
notes = null,
type = 1,
login = ExportJsonData.ExportItem.ItemLoginData(
totp = key,
issuer = issuer,
period = period,
digits = digits,
totp = toOtpAuthUriString(),
),
favorite = false,
)

View File

@@ -16,12 +16,12 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
*/
data class UpdateItemRequest(
val key: String,
val accountName: String,
val accountName: String?,
val type: AuthenticatorItemType,
val algorithm: AuthenticatorItemAlgorithm,
val period: Int,
val digits: Int,
val issuer: String?,
val issuer: String,
) {
/**
* The composite label of the authenticator item. Derived from combining [issuer] and [accountName]
@@ -29,9 +29,9 @@ data class UpdateItemRequest(
* label = accountName /issuer (“:” / “%3A”) *”%20” accountName
* ```
*/
val label = if (issuer != null) {
"$issuer:$accountName"
val label = if (accountName.isNullOrBlank()) {
issuer
} else {
accountName
"$issuer:$accountName"
}
}

View File

@@ -139,17 +139,17 @@ fun EditItemScreen(
.imePadding()
.padding(innerPadding),
viewState = viewState,
onAccountNameTextChange = remember(viewModel) {
onIssuerNameTextChange = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.AccountNameTextChange(it)
EditItemAction.IssuerNameTextChange(it)
)
}
},
onIssuerTextChange = remember(viewModel) {
onUsernameTextChange = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.IssuerTextChange(it)
EditItemAction.UsernameTextChange(it)
)
}
},
@@ -211,8 +211,8 @@ fun EditItemScreen(
fun EditItemContent(
modifier: Modifier = Modifier,
viewState: EditItemState.ViewState.Content,
onAccountNameTextChange: (String) -> Unit = {},
onIssuerTextChange: (String) -> Unit = {},
onIssuerNameTextChange: (String) -> Unit = {},
onUsernameTextChange: (String) -> Unit = {},
onTypeOptionClicked: (AuthenticatorItemType) -> Unit = {},
onTotpCodeTextChange: (String) -> Unit = {},
onAlgorithmOptionClicked: (AuthenticatorItemAlgorithm) -> Unit = {},
@@ -237,8 +237,8 @@ fun EditItemContent(
.fillMaxSize()
.padding(horizontal = 16.dp),
label = stringResource(id = R.string.name),
value = viewState.itemData.accountName,
onValueChange = onAccountNameTextChange,
value = viewState.itemData.issuer,
onValueChange = onIssuerNameTextChange,
singleLine = true,
)
}
@@ -256,20 +256,18 @@ fun EditItemContent(
)
}
viewState.itemData.issuer?.let { issuer ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
label = stringResource(id = R.string.account_info),
value = issuer,
onValueChange = onIssuerTextChange,
label = stringResource(id = R.string.username),
value = viewState.itemData.username.orEmpty(),
onValueChange = onUsernameTextChange,
singleLine = true,
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
@@ -483,7 +481,7 @@ private fun EditItemContentExpandedOptionsPreview() {
refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY,
totpCode = "123456",
type = AuthenticatorItemType.TOTP,
accountName = "account name",
username = "account name",
issuer = "issuer",
algorithm = AuthenticatorItemAlgorithm.SHA1,
digits = VerificationCodeDigitsOption.SIX
@@ -502,7 +500,7 @@ private fun EditItemContentCollapsedOptionsPreview() {
refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY,
totpCode = "123456",
type = AuthenticatorItemType.TOTP,
accountName = "account name",
username = "account name",
issuer = "issuer",
algorithm = AuthenticatorItemAlgorithm.SHA1,
digits = VerificationCodeDigitsOption.SIX

View File

@@ -57,8 +57,8 @@ class EditItemViewModel @Inject constructor(
is EditItemAction.AlgorithmOptionClick -> handleAlgorithmOptionClick(action)
is EditItemAction.CancelClick -> handleCancelClick()
is EditItemAction.TypeOptionClick -> handleTypeOptionClick(action)
is EditItemAction.AccountNameTextChange -> handleAccountNameTextChange(action)
is EditItemAction.IssuerTextChange -> handleIssuerTextChange(action)
is EditItemAction.IssuerNameTextChange -> handleIssuerNameTextChange(action)
is EditItemAction.UsernameTextChange -> handleIssuerTextChange(action)
is EditItemAction.RefreshPeriodOptionClick -> handlePeriodTextChange(action)
is EditItemAction.TotpCodeTextChange -> handleTotpCodeTextChange(action)
is EditItemAction.NumberOfDigitsOptionClick -> handleNumberOfDigitsOptionChange(action)
@@ -76,7 +76,7 @@ class EditItemViewModel @Inject constructor(
}
private fun handleSaveClick() = onContent { content ->
if (content.itemData.accountName.isBlank()) {
if (content.itemData.issuer.isBlank()) {
mutableStateFlow.update {
it.copy(
dialog = EditItemState.DialogState.Generic(
@@ -110,12 +110,12 @@ class EditItemViewModel @Inject constructor(
AuthenticatorItemEntity(
id = state.itemId,
key = content.itemData.totpCode.trim(),
accountName = content.itemData.accountName.trim(),
accountName = content.itemData.username?.trim(),
type = content.itemData.type,
algorithm = content.itemData.algorithm,
period = content.itemData.refreshPeriod.seconds,
digits = content.itemData.digits.length,
issuer = content.itemData.issuer?.trim(),
issuer = content.itemData.issuer.trim(),
)
)
trySendAction(EditItemAction.Internal.UpdateItemResult(result))
@@ -130,18 +130,18 @@ class EditItemViewModel @Inject constructor(
}
}
private fun handleAccountNameTextChange(action: EditItemAction.AccountNameTextChange) {
private fun handleIssuerNameTextChange(action: EditItemAction.IssuerNameTextChange) {
updateItemData { currentItemData ->
currentItemData.copy(
accountName = action.accountName
issuer = action.issuerName
)
}
}
private fun handleIssuerTextChange(action: EditItemAction.IssuerTextChange) {
private fun handleIssuerTextChange(action: EditItemAction.UsernameTextChange) {
updateItemData { currentItemData ->
currentItemData.copy(
issuer = action.issue
username = action.username
)
}
}
@@ -318,7 +318,7 @@ class EditItemViewModel @Inject constructor(
?: AuthenticatorRefreshPeriodOption.THIRTY,
totpCode = key,
type = type,
accountName = accountName,
username = accountName,
issuer = issuer,
algorithm = algorithm,
digits = VerificationCodeDigitsOption.fromIntOrNull(digits)
@@ -435,12 +435,12 @@ sealed class EditItemAction {
/**
* The user has changed the account name text.
*/
data class AccountNameTextChange(val accountName: String) : EditItemAction()
data class IssuerNameTextChange(val issuerName: String) : EditItemAction()
/**
* The user has changed the issue text.
*/
data class IssuerTextChange(val issue: String) : EditItemAction()
data class UsernameTextChange(val username: String) : EditItemAction()
/**
* The user has selected an Item Type option.

View File

@@ -12,7 +12,7 @@ import kotlinx.parcelize.Parcelize
*
* @property refreshPeriod The period for the verification code.
* @property totpCode The totp code for the item.
* @property accountName Account or username for this item.
* @property username Account or username for this item.
* @property issuer Name of the item provider.
* @property algorithm Hashing algorithm used with the item.
* @property digits Number of digits in the verification code.
@@ -22,8 +22,8 @@ data class EditItemData(
val refreshPeriod: AuthenticatorRefreshPeriodOption,
val totpCode: String,
val type: AuthenticatorItemType,
val accountName: String,
val issuer: String?,
val username: String?,
val issuer: String,
val algorithm: AuthenticatorItemAlgorithm,
val digits: VerificationCodeDigitsOption,
) : Parcelable

View File

@@ -31,6 +31,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -228,7 +229,8 @@ fun ItemListingScreen(
items(currentState.itemList) {
VaultVerificationCodeItem(
authCode = it.authCode,
issuer = it.issuer,
name = it.issuer,
username = it.username,
periodSeconds = it.periodSeconds,
timeLeftSeconds = it.timeLeftSeconds,
alertThresholdSeconds = it.alertThresholdSeconds,
@@ -257,7 +259,6 @@ fun ItemListingScreen(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
supportingLabel = it.supportingLabel,
)
}
}

View File

@@ -391,6 +391,13 @@ class ItemListingViewModel @Inject constructor(
?: return null
val label = uri.pathSegments.firstOrNull() ?: return null
val accountName = if (label.contains(":")) {
label
.split(":")
.last()
} else {
label
}
val key = uri.getQueryParameter(SECRET) ?: return null
@@ -401,14 +408,14 @@ class ItemListingViewModel @Inject constructor(
val digits = uri.getQueryParameter(DIGITS)?.toInt() ?: 6
val issuer = uri.getQueryParameter(ISSUER)
val issuer = uri.getQueryParameter(ISSUER) ?: label
val period = uri.getQueryParameter(PERIOD)?.toInt() ?: 30
return AuthenticatorItemEntity(
id = UUID.randomUUID().toString(),
key = key,
accountName = label,
accountName = accountName,
type = type,
algorithm = algorithm,
period = period,

View File

@@ -39,20 +39,21 @@ import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
* The verification code item displayed to the user.
*
* @param authCode The code for the item.
* @param issuer The label for the item.
* @param name The label for the item. Represents the OTP issuer.
* @param username The supporting label for the item. Represents the OTP account name.
* @param periodSeconds The times span where the code is valid.
* @param timeLeftSeconds The seconds remaining until a new code is needed.
* @param startIcon The leading icon for the item.
* @param onItemClick The lambda function to be invoked when the item is clicked.
* @param modifier The modifier for the item.
* @param supportingLabel The supporting label for the item.
*/
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "MagicNumber")
@Composable
fun VaultVerificationCodeItem(
authCode: String,
issuer: String?,
name: String?,
username: String?,
periodSeconds: Int,
timeLeftSeconds: Int,
alertThresholdSeconds: Int,
@@ -61,7 +62,6 @@ fun VaultVerificationCodeItem(
onEditItemClick: () -> Unit,
onDeleteItemClick: () -> Unit,
modifier: Modifier = Modifier,
supportingLabel: String? = null,
) {
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
Box(modifier = modifier) {
@@ -93,9 +93,9 @@ fun VaultVerificationCodeItem(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.weight(1f),
) {
issuer?.let {
if (!name.isNullOrEmpty()) {
Text(
text = it,
text = name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -103,9 +103,9 @@ fun VaultVerificationCodeItem(
)
}
supportingLabel?.let {
if (!username.isNullOrEmpty()) {
Text(
text = it,
text = username,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
@@ -173,7 +173,8 @@ private fun VerificationCodeItem_preview() {
AuthenticatorTheme {
VaultVerificationCodeItem(
authCode = "1234567890".chunked(3).joinToString(" "),
issuer = "Sample Label",
name = "Issuer, AKA Name",
username = "username@bitwarden.com",
periodSeconds = 30,
timeLeftSeconds = 15,
alertThresholdSeconds = 7,
@@ -182,7 +183,6 @@ private fun VerificationCodeItem_preview() {
onEditItemClick = {},
onDeleteItemClick = {},
modifier = Modifier.padding(horizontal = 16.dp),
supportingLabel = "Supporting Label",
)
}
}

View File

@@ -11,9 +11,8 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class VerificationCodeDisplayItem(
val id: String,
val label: String,
val issuer: String?,
val supportingLabel: String?,
val username: String?,
val timeLeftSeconds: Int,
val periodSeconds: Int,
val alertThresholdSeconds: Int,

View File

@@ -18,9 +18,8 @@ fun List<VerificationCodeItem>.toViewState(
fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) =
VerificationCodeDisplayItem(
id = id,
label = label,
issuer = issuer,
supportingLabel = username,
username = username,
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,

View File

@@ -133,7 +133,7 @@ fun ManualCodeEntryScreen(
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = state.accountName,
value = state.issuer,
onValueChange = remember(viewModel) {
{
viewModel.trySendAction(

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
@@ -29,7 +30,7 @@ class ManualCodeEntryViewModel @Inject constructor(
private val authenticatorRepository: AuthenticatorRepository,
) : BaseViewModel<ManualCodeEntryState, ManualCodeEntryEvent, ManualCodeEntryAction>(
initialState = savedStateHandle[KEY_STATE]
?: ManualCodeEntryState(code = "", accountName = "", dialog = null),
?: ManualCodeEntryState(code = "", issuer = "", dialog = null),
) {
override fun handleAction(action: ManualCodeEntryAction) {
when (action) {
@@ -47,7 +48,7 @@ class ManualCodeEntryViewModel @Inject constructor(
private fun handleIssuerTextChange(action: ManualCodeEntryAction.IssuerTextChange) {
mutableStateFlow.update {
it.copy(accountName = action.accountName)
it.copy(issuer = action.issuer)
}
}
@@ -67,8 +68,14 @@ class ManualCodeEntryViewModel @Inject constructor(
AuthenticatorItemEntity(
id = UUID.randomUUID().toString(),
key = state.code,
accountName = state.accountName,
issuer = state.issuer,
accountName = "",
userId = null,
type = if (state.code.startsWith("steam://")) {
AuthenticatorItemType.STEAM
} else {
AuthenticatorItemType.TOTP
}
)
)
sendAction(ManualCodeEntryAction.Internal.CreateItemResultReceive(result))
@@ -118,7 +125,7 @@ class ManualCodeEntryViewModel @Inject constructor(
@Parcelize
data class ManualCodeEntryState(
val code: String,
val accountName: String,
val issuer: String,
val dialog: DialogState?,
) : Parcelable {
@@ -187,7 +194,7 @@ sealed class ManualCodeEntryAction {
/**
* The use has changed the issuer text.
*/
data class IssuerTextChange(val accountName: String) : ManualCodeEntryAction()
data class IssuerTextChange(val issuer: String) : ManualCodeEntryAction()
/**
* Models actions that the [ManualCodeEntryViewModel] itself might send.

View File

@@ -34,7 +34,7 @@
<string name="add_code">Add code</string>
<string name="authenticator_key_read_error">Cannot read authenticator key.</string>
<string name="verification_code_added">Verification code added</string>
<string name="account_info">Account info</string>
<string name="username">Username</string>
<string name="refresh_period">Refresh period</string>
<string name="algorithm">Algorithm</string>
<string name="hide">Hide</string>