BIT-279: Adding password history data layer (#387)

This commit is contained in:
joshua-livefront
2023-12-14 12:25:35 -05:00
committed by Álison Fernandes
parent 0655f74479
commit 09fbd5d4e9
20 changed files with 693 additions and 2 deletions

View File

@@ -0,0 +1,34 @@
package com.x8bit.bitwarden.data.platform.repository.model
/**
* A local data state used for handling local data in the repository layer.
*/
sealed class LocalDataState<out T> {
/**
* Data that is being wrapped by [LocalDataState].
*/
abstract val data: T?
/**
* Loading state representing the absence of data.
*/
data object Loading : LocalDataState<Nothing>() {
override val data: Nothing? get() = null
}
/**
* Loaded state representing the availability of data.
*/
data class Loaded<T>(
override val data: T,
) : LocalDataState<T>()
/**
* Error state that may or may not have data available.
*/
data class Error<T>(
val error: Throwable,
override val data: T? = null,
) : LocalDataState<T>()
}

View File

@@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
import kotlinx.coroutines.flow.Flow
/**
* Primary access point for disk information related to password history.
*/
interface PasswordHistoryDiskSource {
/**
* Retrieves all password history items from the data source as a Flow.
*/
fun getPasswordHistoriesForUser(userId: String): Flow<List<PasswordHistoryEntity>>
/**
* Inserts a generated history item into the data source.
*/
suspend fun insertPasswordHistory(passwordHistoryEntity: PasswordHistoryEntity)
/**
* Clears all password history items from the data source.
*/
suspend fun clearPasswordHistories(userId: String)
}

View File

@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.PasswordHistoryDao
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
import kotlinx.coroutines.flow.Flow
/**
* Primary implementation of [PasswordHistoryDiskSource].
*/
class PasswordHistoryDiskSourceImpl(
private val passwordHistoryDao: PasswordHistoryDao,
) : PasswordHistoryDiskSource {
override fun getPasswordHistoriesForUser(userId: String): Flow<List<PasswordHistoryEntity>> {
return passwordHistoryDao.getPasswordHistoriesForUserAsFlow(userId)
}
override suspend fun insertPasswordHistory(
passwordHistoryEntity: PasswordHistoryEntity,
) {
passwordHistoryDao.insertPasswordHistory(passwordHistoryEntity)
}
override suspend fun clearPasswordHistories(userId: String) {
passwordHistoryDao.clearPasswordHistoriesForUser(userId)
}
}

View File

@@ -0,0 +1,33 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
import kotlinx.coroutines.flow.Flow
/**
* Provides methods for inserting, retrieving, and deleting passcode history items
* from the database, interacting with the [PasswordHistoryEntity] entity.
*/
@Dao
interface PasswordHistoryDao {
/**
* Inserts a password history item into the database.
*/
@Insert
suspend fun insertPasswordHistory(passwordHistory: PasswordHistoryEntity)
/**
* Retrieves all password history items for a specific user from the database as a Flow.
*/
@Query("SELECT * FROM password_history WHERE userId = :userId")
fun getPasswordHistoriesForUserAsFlow(userId: String): Flow<List<PasswordHistoryEntity>>
/**
* Clears all password history items from the database for a specific user.
*/
@Query("DELETE FROM password_history WHERE userId = :userId")
suspend fun clearPasswordHistoriesForUser(userId: String)
}

View File

@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.PasswordHistoryDao
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
/**
* Room database for storing passcode history.
*/
@Database(entities = [PasswordHistoryEntity::class], version = 1)
abstract class PasswordHistoryDatabase : RoomDatabase() {
/**
* Provides the DAO for accessing passcode history data.
*/
abstract fun passwordHistoryDao(): PasswordHistoryDao
}

View File

@@ -1,8 +1,14 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.di
import android.app.Application
import android.content.SharedPreferences
import androidx.room.Room
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.PasswordHistoryDao
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.database.PasswordHistoryDatabase
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSourceImpl
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSourceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -27,4 +33,30 @@ object GeneratorDiskModule {
sharedPreferences = sharedPreferences,
json = json,
)
@Provides
@Singleton
fun providePasswordHistoryDiskSource(
passwordHistoryDao: PasswordHistoryDao,
): PasswordHistoryDiskSource = PasswordHistoryDiskSourceImpl(
passwordHistoryDao = passwordHistoryDao,
)
@Provides
@Singleton
fun providePasswordHistoryDatabase(app: Application): PasswordHistoryDatabase {
return Room
.databaseBuilder(
context = app,
klass = PasswordHistoryDatabase::class.java,
name = "passcode_history_database",
)
.build()
}
@Provides
@Singleton
fun providePasswordHistoryDao(database: PasswordHistoryDatabase): PasswordHistoryDao {
return database.passwordHistoryDao()
}
}

View File

@@ -0,0 +1,49 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.bitwarden.core.PasswordHistory
import java.time.Instant
/**
* Entity representing a generated history item in the database.
*/
@Entity(tableName = "password_history")
data class PasswordHistoryEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Int = 0,
@ColumnInfo(name = "userId")
val userId: String,
@ColumnInfo(name = "encrypted_password")
val encryptedPassword: String,
@ColumnInfo(name = "generated_date_time_ms")
val generatedDateTimeMs: Long,
)
/**
* Converts a PasswordHistory object to a GeneratedHistoryItem.
* This function is used to transform data from the SDK model to the database entity model.
*/
fun PasswordHistory.toPasswordHistoryEntity(userId: String): PasswordHistoryEntity {
return PasswordHistoryEntity(
userId = userId,
encryptedPassword = this.password,
generatedDateTimeMs = this.lastUsedDate.toEpochMilli(),
)
}
/**
* Converts a GeneratedHistoryItem object to a PasswordHistory.
* This function is used to transform data from the database entity model to the SDK model.
*/
fun PasswordHistoryEntity.toPasswordHistory(): PasswordHistory {
return PasswordHistory(
password = this.encryptedPassword,
lastUsedDate = Instant.ofEpochMilli(this.generatedDateTimeMs),
)
}

View File

@@ -2,15 +2,23 @@ package com.x8bit.bitwarden.data.tools.generator.repository
import com.bitwarden.core.PassphraseGeneratorRequest
import com.bitwarden.core.PasswordGeneratorRequest
import com.bitwarden.core.PasswordHistoryView
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import kotlinx.coroutines.flow.StateFlow
/**
* Responsible for managing generator data.
*/
interface GeneratorRepository {
/**
* Retrieve all stored password history items for the current user.
*/
val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
/**
* Attempt to generate a password.
*/
@@ -34,4 +42,14 @@ interface GeneratorRepository {
* Save the [PasscodeGenerationOptions] for the current user.
*/
fun savePasscodeGenerationOptions(options: PasscodeGenerationOptions)
/**
* Store a password history item for the current user.
*/
suspend fun storePasswordHistory(passwordHistoryView: PasswordHistoryView)
/**
* Clear all stored password history for the current user.
*/
suspend fun clearPasswordHistory()
}

View File

@@ -2,24 +2,103 @@ package com.x8bit.bitwarden.data.tools.generator.repository
import com.bitwarden.core.PassphraseGeneratorRequest
import com.bitwarden.core.PasswordGeneratorRequest
import com.bitwarden.core.PasswordHistoryView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.toPasswordHistory
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.toPasswordHistoryEntity
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Singleton
/**
* Default implementation of [GeneratorRepository].
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class GeneratorRepositoryImpl constructor(
private val generatorSdkSource: GeneratorSdkSource,
private val generatorDiskSource: GeneratorDiskSource,
private val authDiskSource: AuthDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
) : GeneratorRepository {
private val scope = CoroutineScope(Dispatchers.IO)
private val mutablePasswordHistoryStateFlow =
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
get() = mutablePasswordHistoryStateFlow.asStateFlow()
private var passwordHistoryJob: Job? = null
init {
mutablePasswordHistoryStateFlow
.subscriptionCount
.flatMapLatest { subscriberCount ->
if (subscriberCount > 0) {
authDiskSource
.userStateFlow
.map { it?.activeUserId }
.distinctUntilChanged()
} else {
flow { awaitCancellation() }
}
}
.onEach { activeUserId ->
observePasswordHistoryForUser(activeUserId)
}
.launchIn(scope)
}
private fun observePasswordHistoryForUser(userId: String?) {
passwordHistoryJob?.cancel()
userId ?: return
mutablePasswordHistoryStateFlow.value = LocalDataState.Loading
passwordHistoryJob = passwordHistoryDiskSource
.getPasswordHistoriesForUser(userId)
.map { encryptedPasswordHistoryList ->
val passwordHistories =
encryptedPasswordHistoryList.map { it.toPasswordHistory() }
vaultSdkSource
.decryptPasswordHistoryList(passwordHistories)
}
.onEach { encryptedPasswordHistoryListResult ->
encryptedPasswordHistoryListResult
.fold(
onSuccess = {
mutablePasswordHistoryStateFlow.value = LocalDataState.Loaded(it)
},
onFailure = {
mutablePasswordHistoryStateFlow.value = LocalDataState.Error(it)
},
)
}
.launchIn(scope)
}
override suspend fun generatePassword(
passwordGeneratorRequest: PasswordGeneratorRequest,
): GeneratedPasswordResult =
@@ -49,4 +128,19 @@ class GeneratorRepositoryImpl constructor(
val userId = authDiskSource.userState?.activeUserId
userId?.let { generatorDiskSource.storePasscodeGenerationOptions(it, options) }
}
override suspend fun storePasswordHistory(passwordHistoryView: PasswordHistoryView) {
val userId = authDiskSource.userState?.activeUserId ?: return
val encryptedPasswordHistory = vaultSdkSource
.encryptPasswordHistory(passwordHistoryView)
.getOrNull() ?: return
passwordHistoryDiskSource.insertPasswordHistory(
encryptedPasswordHistory.toPasswordHistoryEntity(userId),
)
}
override suspend fun clearPasswordHistory() {
val userId = authDiskSource.userState?.activeUserId ?: return
passwordHistoryDiskSource.clearPasswordHistories(userId)
}
}

View File

@@ -2,9 +2,11 @@ package com.x8bit.bitwarden.data.tools.generator.repository.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepositoryImpl
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -24,9 +26,13 @@ object GeneratorRepositoryModule {
generatorSdkSource: GeneratorSdkSource,
generatorDiskSource: GeneratorDiskSource,
authDiskSource: AuthDiskSource,
vaultSdkSource: VaultSdkSource,
passwordHistoryDiskSource: PasswordHistoryDiskSource,
): GeneratorRepository = GeneratorRepositoryImpl(
generatorSdkSource = generatorSdkSource,
generatorDiskSource = generatorDiskSource,
authDiskSource = authDiskSource,
vaultSdkSource = vaultSdkSource,
passwordHistoryDiskSource = passwordHistoryDiskSource,
)
}

View File

@@ -9,6 +9,8 @@ import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.Send
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
@@ -86,4 +88,18 @@ interface VaultSdkSource {
* Decrypts a list of [Folder]s returning a list of [FolderView] wrapped in a [Result].
*/
suspend fun decryptFolderList(folderList: List<Folder>): Result<List<FolderView>>
/**
* Encrypts a given password history item.
*/
suspend fun encryptPasswordHistory(
passwordHistory: PasswordHistoryView,
): Result<PasswordHistory>
/**
* Decrypts a list of password history items.
*/
suspend fun decryptPasswordHistoryList(
passwordHistoryList: List<PasswordHistory>,
): Result<List<PasswordHistoryView>>
}

View File

@@ -9,10 +9,13 @@ import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.Send
import com.bitwarden.core.SendView
import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.ClientCrypto
import com.bitwarden.sdk.ClientPasswordHistory
import com.bitwarden.sdk.ClientVault
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
@@ -24,6 +27,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResul
class VaultSdkSourceImpl(
private val clientVault: ClientVault,
private val clientCrypto: ClientCrypto,
private val clientPasswordHistory: ClientPasswordHistory,
) : VaultSdkSource {
override suspend fun initializeCrypto(
request: InitUserCryptoRequest,
@@ -88,4 +92,16 @@ class VaultSdkSourceImpl(
override suspend fun decryptFolderList(folderList: List<Folder>): Result<List<FolderView>> =
runCatching { clientVault.folders().decryptList(folderList) }
override suspend fun encryptPasswordHistory(
passwordHistory: PasswordHistoryView,
): Result<PasswordHistory> = runCatching {
clientPasswordHistory.encrypt(passwordHistory)
}
override suspend fun decryptPasswordHistoryList(
passwordHistoryList: List<PasswordHistory>,
): Result<List<PasswordHistoryView>> = runCatching {
clientPasswordHistory.decryptList(passwordHistoryList)
}
}

View File

@@ -24,5 +24,6 @@ object VaultSdkModule {
VaultSdkSourceImpl(
clientVault = client.vault(),
clientCrypto = client.crypto(),
clientPasswordHistory = client.vault().passwordHistory(),
)
}