mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 03:08:50 +03:00
BIT-279: Adding password history data layer (#387)
This commit is contained in:
parent
0655f74479
commit
09fbd5d4e9
20 changed files with 693 additions and 2 deletions
|
@ -160,6 +160,8 @@ koverReport {
|
|||
"com.x8bit.bitwarden.MainActivity*",
|
||||
// Empty Composables
|
||||
"com.x8bit.bitwarden.ui.platform.feature.splash.SplashScreenKt",
|
||||
// Databases
|
||||
"*.database.*Database",
|
||||
)
|
||||
packages(
|
||||
// Dependency injection
|
||||
|
|
|
@ -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>()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,5 +24,6 @@ object VaultSdkModule {
|
|||
VaultSdkSourceImpl(
|
||||
clientVault = client.vault(),
|
||||
clientCrypto = client.crypto(),
|
||||
clientPasswordHistory = client.vault().passwordHistory(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.FakePasswordHistoryDao
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class PasswordHistoryDiskSourceTest {
|
||||
|
||||
private val fakePasswordHistoryDao = FakePasswordHistoryDao()
|
||||
private val diskSource = PasswordHistoryDiskSourceImpl(fakePasswordHistoryDao)
|
||||
private val testUserId = "testUserId"
|
||||
|
||||
@Test
|
||||
fun `insertPassword calls dao insertPasswordHistory`() = runTest {
|
||||
val passwordHistoryEntity = PasswordHistoryEntity(
|
||||
id = 0,
|
||||
userId = testUserId,
|
||||
encryptedPassword = "encrypted",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
|
||||
diskSource.insertPasswordHistory(passwordHistoryEntity)
|
||||
|
||||
assertTrue(fakePasswordHistoryDao.storedPasswordHistories.contains(passwordHistoryEntity))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasswordHistoriesForUser returns flow from dao`() = runTest {
|
||||
val passwordHistoryEntity = PasswordHistoryEntity(
|
||||
id = 0,
|
||||
userId = testUserId,
|
||||
encryptedPassword = "encrypted",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
fakePasswordHistoryDao.insertPasswordHistory(passwordHistoryEntity)
|
||||
|
||||
val result = diskSource
|
||||
.getPasswordHistoriesForUser(testUserId)
|
||||
.first()
|
||||
|
||||
assertEquals(listOf(passwordHistoryEntity), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearPasswordHistoriesForUser calls dao clearPasswordHistoriesForUser`() = runTest {
|
||||
fakePasswordHistoryDao.storedPasswordHistories.add(
|
||||
PasswordHistoryEntity(
|
||||
id = 1,
|
||||
userId = testUserId,
|
||||
encryptedPassword = "encrypted",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
),
|
||||
)
|
||||
|
||||
diskSource.clearPasswordHistories(testUserId)
|
||||
|
||||
assertTrue(fakePasswordHistoryDao.storedPasswordHistories.none { it.userId == testUserId })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao
|
||||
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class FakePasswordHistoryDao : PasswordHistoryDao {
|
||||
val storedPasswordHistories = mutableListOf<PasswordHistoryEntity>()
|
||||
|
||||
private val passwordHistoriesFlow = MutableSharedFlow<List<PasswordHistoryEntity>>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
init {
|
||||
passwordHistoriesFlow.tryEmit(emptyList())
|
||||
}
|
||||
|
||||
override suspend fun insertPasswordHistory(passwordHistory: PasswordHistoryEntity) {
|
||||
storedPasswordHistories.add(passwordHistory)
|
||||
passwordHistoriesFlow.tryEmit(storedPasswordHistories.toList())
|
||||
}
|
||||
|
||||
override fun getPasswordHistoriesForUserAsFlow(
|
||||
userId: String,
|
||||
): Flow<List<PasswordHistoryEntity>> {
|
||||
return passwordHistoriesFlow
|
||||
.map { histories -> histories.filter { it.userId == userId } }
|
||||
}
|
||||
|
||||
override suspend fun clearPasswordHistoriesForUser(userId: String) {
|
||||
storedPasswordHistories.removeAll { it.userId == userId }
|
||||
passwordHistoriesFlow.tryEmit(storedPasswordHistories.toList())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity
|
||||
|
||||
import com.bitwarden.core.PasswordHistory
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import java.time.Instant
|
||||
|
||||
class PasswordHistoryEntityTest {
|
||||
|
||||
@Test
|
||||
fun `toPasswordHistoryEntity should return the correct value`() {
|
||||
val passwordHistory = PasswordHistory(
|
||||
password = "testPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
val expectedEntity = PasswordHistoryEntity(
|
||||
id = 0,
|
||||
userId = "testId",
|
||||
encryptedPassword = "testPassword",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
val entity = passwordHistory.toPasswordHistoryEntity("testId")
|
||||
|
||||
assertEquals(expectedEntity, entity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toPasswordHistory should return the correct value`() {
|
||||
val entity = PasswordHistoryEntity(
|
||||
id = 1,
|
||||
userId = "testId",
|
||||
encryptedPassword = "testPassword",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
val passwordHistory = entity.toPasswordHistory()
|
||||
val expectedPasswordHistory = PasswordHistory(
|
||||
password = "testPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
|
||||
assertEquals(expectedPasswordHistory, passwordHistory)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package com.x8bit.bitwarden.data.tools.generator.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.PassphraseGeneratorRequest
|
||||
import com.bitwarden.core.PasswordGeneratorRequest
|
||||
import com.bitwarden.core.PasswordHistory
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
|
@ -11,34 +14,45 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
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.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 io.mockk.Runs
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class GeneratorRepositoryTest {
|
||||
|
||||
private val generatorSdkSource: GeneratorSdkSource = mockk()
|
||||
private val generatorDiskSource: GeneratorDiskSource = mockk()
|
||||
private val authDiskSource: AuthDiskSource = mockk()
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource = mockk()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||
|
||||
private val repository = GeneratorRepositoryImpl(
|
||||
generatorSdkSource = generatorSdkSource,
|
||||
generatorDiskSource = generatorDiskSource,
|
||||
authDiskSource = authDiskSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
|
@ -213,13 +227,105 @@ class GeneratorRepositoryTest {
|
|||
|
||||
coEvery {
|
||||
generatorDiskSource.storePasscodeGenerationOptions(userId, optionsToSave)
|
||||
} just Runs
|
||||
} just runs
|
||||
|
||||
repository.savePasscodeGenerationOptions(optionsToSave)
|
||||
|
||||
coVerify { generatorDiskSource.storePasscodeGenerationOptions(userId, optionsToSave) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePasswordHistory should call encrypt and insert functions`() = runTest {
|
||||
val testUserId = "testUserId"
|
||||
val passwordHistoryView = PasswordHistoryView(
|
||||
password = "decryptedPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
val encryptedPasswordHistory = PasswordHistory(
|
||||
password = "encryptedPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
val expectedPasswordHistoryEntity = encryptedPasswordHistory
|
||||
.toPasswordHistoryEntity(testUserId)
|
||||
|
||||
coEvery { authDiskSource.userState?.activeUserId } returns testUserId
|
||||
|
||||
coEvery { vaultSdkSource.encryptPasswordHistory(passwordHistoryView) } returns
|
||||
Result.success(encryptedPasswordHistory)
|
||||
|
||||
coEvery {
|
||||
passwordHistoryDiskSource.insertPasswordHistory(expectedPasswordHistoryEntity)
|
||||
} just runs
|
||||
|
||||
repository.storePasswordHistory(passwordHistoryView)
|
||||
|
||||
coVerify { vaultSdkSource.encryptPasswordHistory(passwordHistoryView) }
|
||||
coVerify { passwordHistoryDiskSource.insertPasswordHistory(expectedPasswordHistoryEntity) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordHistoryStateFlow should emit correct states based on password history updates`() =
|
||||
runTest {
|
||||
val encryptedPasswordHistoryEntities = listOf(
|
||||
PasswordHistoryEntity(
|
||||
userId = USER_STATE.activeUserId,
|
||||
encryptedPassword = "encryptedPassword1",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
),
|
||||
PasswordHistoryEntity(
|
||||
userId = USER_STATE.activeUserId,
|
||||
encryptedPassword = "encryptedPassword2",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-02T00:00:00Z").toEpochMilli(),
|
||||
),
|
||||
)
|
||||
|
||||
val decryptedPasswordHistoryList = listOf(
|
||||
PasswordHistoryView(
|
||||
password = "password1",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
),
|
||||
PasswordHistoryView(
|
||||
password = "password2",
|
||||
lastUsedDate = Instant.parse("2021-01-02T00:00:00Z"),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery { authDiskSource.userStateFlow } returns flowOf(USER_STATE)
|
||||
|
||||
coEvery {
|
||||
passwordHistoryDiskSource.getPasswordHistoriesForUser(USER_STATE.activeUserId)
|
||||
} returns flowOf(encryptedPasswordHistoryEntities)
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.decryptPasswordHistoryList(any())
|
||||
} returns Result.success(decryptedPasswordHistoryList)
|
||||
|
||||
val historyFlow = repository.passwordHistoryStateFlow
|
||||
|
||||
historyFlow.test {
|
||||
assertEquals(LocalDataState.Loading, awaitItem())
|
||||
assertEquals(LocalDataState.Loaded(decryptedPasswordHistoryList), awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
coVerify {
|
||||
passwordHistoryDiskSource.getPasswordHistoriesForUser(USER_STATE.activeUserId)
|
||||
}
|
||||
|
||||
coVerify { vaultSdkSource.decryptPasswordHistoryList(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearPasswordHistory should call clearAllPasswords function`() = runTest {
|
||||
val testUserId = "testUserId"
|
||||
coEvery { authDiskSource.userState?.activeUserId } returns testUserId
|
||||
coEvery { passwordHistoryDiskSource.clearPasswordHistories(testUserId) } just runs
|
||||
|
||||
repository.clearPasswordHistory()
|
||||
|
||||
coVerify { passwordHistoryDiskSource.clearPasswordHistories(testUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `savePasscodeGenerationOptions should not store options when there is no active user`() =
|
||||
runTest {
|
||||
|
|
|
@ -2,10 +2,14 @@ package com.x8bit.bitwarden.data.tools.generator.repository.util
|
|||
|
||||
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.GeneratorRepository
|
||||
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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A fake implementation of [GeneratorRepository] for testing purposes.
|
||||
|
@ -21,6 +25,12 @@ class FakeGeneratorRepository : GeneratorRepository {
|
|||
)
|
||||
private var passcodeGenerationOptions: PasscodeGenerationOptions? = null
|
||||
|
||||
private val mutablePasswordHistoryStateFlow =
|
||||
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
|
||||
|
||||
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
||||
get() = mutablePasswordHistoryStateFlow
|
||||
|
||||
override suspend fun generatePassword(
|
||||
passwordGeneratorRequest: PasswordGeneratorRequest,
|
||||
): GeneratedPasswordResult {
|
||||
|
@ -41,6 +51,16 @@ class FakeGeneratorRepository : GeneratorRepository {
|
|||
passcodeGenerationOptions = options
|
||||
}
|
||||
|
||||
override suspend fun storePasswordHistory(passwordHistoryView: PasswordHistoryView) {
|
||||
val currentList = mutablePasswordHistoryStateFlow.value.data.orEmpty()
|
||||
val updatedList = currentList + passwordHistoryView
|
||||
mutablePasswordHistoryStateFlow.value = LocalDataState.Loaded(updatedList)
|
||||
}
|
||||
|
||||
override suspend fun clearPasswordHistory() {
|
||||
mutablePasswordHistoryStateFlow.value = LocalDataState.Loaded(emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mock result for the generatePassword function.
|
||||
*/
|
||||
|
|
|
@ -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.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
|
@ -27,9 +30,11 @@ import org.junit.jupiter.api.Test
|
|||
class VaultSdkSourceTest {
|
||||
private val clientVault = mockk<ClientVault>()
|
||||
private val clientCrypto = mockk<ClientCrypto>()
|
||||
private val clientPasswordHistory = mockk<ClientPasswordHistory>()
|
||||
private val vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
clientVault = clientVault,
|
||||
clientCrypto = clientCrypto,
|
||||
clientPasswordHistory = clientPasswordHistory,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -404,4 +409,50 @@ class VaultSdkSourceTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encryptPasswordHistory should call SDK and return a Result with correct data`() =
|
||||
runBlocking {
|
||||
val mockPasswordHistoryView = mockk<PasswordHistoryView>()
|
||||
val expectedResult = mockk<PasswordHistory>()
|
||||
coEvery {
|
||||
clientPasswordHistory.encrypt(
|
||||
passwordHistory = mockPasswordHistoryView,
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = vaultSdkSource.encryptPasswordHistory(
|
||||
passwordHistory = mockPasswordHistoryView,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult.asSuccess(), result)
|
||||
coVerify {
|
||||
clientPasswordHistory.encrypt(
|
||||
passwordHistory = mockPasswordHistoryView,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decryptPasswordHistoryList should call SDK and return a Result with correct data`() =
|
||||
runBlocking {
|
||||
val mockPasswordHistoryList = mockk<List<PasswordHistory>>()
|
||||
val expectedResult = mockk<List<PasswordHistoryView>>()
|
||||
coEvery {
|
||||
clientPasswordHistory.decryptList(
|
||||
list = mockPasswordHistoryList,
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = vaultSdkSource.decryptPasswordHistoryList(
|
||||
passwordHistoryList = mockPasswordHistoryList,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult.asSuccess(), result)
|
||||
coVerify {
|
||||
clientPasswordHistory.decryptList(
|
||||
list = mockPasswordHistoryList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue