BIT-990: Initialize Crypto for Vault (#213)

This commit is contained in:
Ramsey Smith
2023-11-07 09:17:32 -07:00
committed by Álison Fernandes
parent 7fc571bb92
commit a9295ff981
16 changed files with 481 additions and 14 deletions

View File

@@ -18,10 +18,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -30,22 +32,20 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
import javax.inject.Singleton
private const val DEFAULT_KDF_ITERATIONS = 600000
/**
* Default implementation of [AuthRepository].
*/
@Suppress("LongParameterList")
@Singleton
class AuthRepositoryImpl @Inject constructor(
class AuthRepositoryImpl constructor(
private val accountsService: AccountsService,
private val haveIBeenPwnedService: HaveIBeenPwnedService,
private val identityService: IdentityService,
private val authSdkSource: AuthSdkSource,
private val authDiskSource: AuthDiskSource,
private val vaultRepository: VaultRepository,
dispatcherManager: DispatcherManager,
) : AuthRepository {
private val scope = CoroutineScope(dispatcherManager.io)
@@ -122,6 +122,7 @@ class AuthRepositoryImpl @Inject constructor(
privateKey = it.privateKey,
)
}
vaultRepository.unlockVaultAndSync(masterPassword = password)
LoginResult.Success
}
@@ -176,7 +177,7 @@ class AuthRepositoryImpl @Inject constructor(
}
}
}
val kdf = Kdf.Pbkdf2(DEFAULT_KDF_ITERATIONS.toUInt())
val kdf = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt())
return authSdkSource
.makeRegisterKeys(
email = email,

View File

@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -31,6 +32,7 @@ object AuthRepositoryModule {
authSdkSource: AuthSdkSource,
authDiskSource: AuthDiskSource,
dispatchers: DispatcherManager,
vaultRepository: VaultRepository,
): AuthRepository = AuthRepositoryImpl(
accountsService = accountsService,
identityService = identityService,
@@ -38,5 +40,6 @@ object AuthRepositoryModule {
authDiskSource = authDiskSource,
haveIBeenPwnedService = haveIBeenPwnedService,
dispatcherManager = dispatchers,
vaultRepository = vaultRepository,
)
}

View File

@@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants
/**
* Convert [AccountJson.Profile] to [Kdf] params for use with Bitwarden SDK.
*/
fun AccountJson.Profile.toSdkParams(): Kdf {
return when (this.kdfType) {
KdfTypeJson.ARGON2_ID -> Kdf.Argon2id(
iterations = (kdfIterations ?: KdfParamsConstants.DEFAULT_ARGON2_ITERATIONS).toUInt(),
memory = (kdfMemory ?: KdfParamsConstants.DEFAULT_ARGON2_MEMORY).toUInt(),
parallelism =
(kdfParallelism ?: KdfParamsConstants.DEFAULT_ARGON2_PARALLELISM).toUInt(),
)
KdfTypeJson.PBKDF2_SHA256 -> Kdf.Pbkdf2(
iterations = (kdfIterations ?: KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS).toUInt(),
)
else -> Kdf.Pbkdf2(iterations = KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS.toUInt())
}
}

View File

@@ -0,0 +1,29 @@
package com.x8bit.bitwarden.data.auth.util
import com.bitwarden.core.Kdf
/**
* Constants relating to [Kdf] initialization defaults.
*/
object KdfParamsConstants {
/**
* The default number of iterations when calculating a user's password for [Kdf.Pbkdf2].
*/
const val DEFAULT_PBKDF2_ITERATIONS: Int = 600000
/**
* The default number of iterations when calculating a user's password for [Kdf.Argon2id].
*/
const val DEFAULT_ARGON2_ITERATIONS: Int = 3
/**
* The default amount of memory to use when calculating a password hash (MB) for [Kdf.Argon2id].
*/
const val DEFAULT_ARGON2_MEMORY: Int = 64
/**
* The default number of threads to use when calculating a password hash for [Kdf.Argon2id].
*/
const val DEFAULT_ARGON2_PARALLELISM: Int = 4
}

View File

@@ -5,11 +5,20 @@ import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitCryptoRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
/**
* Source of vault information and functionality from the Bitwarden SDK.
*/
interface VaultSdkSource {
/**
* Attempts to initialize cryptography functionality for the Bitwarden SDK
* with a given [InitCryptoRequest].
*/
suspend fun initializeCrypto(request: InitCryptoRequest): Result<InitializeCryptoResult>
/**
* Decrypts a [Cipher] returning a [CipherView] wrapped in a [Result].
*/

View File

@@ -5,7 +5,11 @@ import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitCryptoRequest
import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.ClientCrypto
import com.bitwarden.sdk.ClientVault
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
/**
* Primary implementation of [VaultSdkSource] that serves as a convenience wrapper around a
@@ -13,7 +17,20 @@ import com.bitwarden.sdk.ClientVault
*/
class VaultSdkSourceImpl(
private val clientVault: ClientVault,
private val clientCrypto: ClientCrypto,
) : VaultSdkSource {
override suspend fun initializeCrypto(
request: InitCryptoRequest,
): Result<InitializeCryptoResult> =
runCatching {
try {
clientCrypto.initializeCrypto(req = request)
InitializeCryptoResult.Success
} catch (exception: BitwardenException) {
// The only truly expected error from the SDK is an incorrect password.
InitializeCryptoResult.AuthenticationError
}
}
override suspend fun decryptCipher(cipher: Cipher): Result<CipherView> =
runCatching { clientVault.ciphers().decrypt(cipher) }

View File

@@ -14,11 +14,15 @@ import javax.inject.Singleton
*/
@Module
@InstallIn(SingletonComponent::class)
class VaultSdkModule {
object VaultSdkModule {
@Provides
@Singleton
fun providesVaultSdkSource(
client: Client,
): VaultSdkSource = VaultSdkSourceImpl(clientVault = client.vault())
): VaultSdkSource =
VaultSdkSourceImpl(
clientVault = client.vault(),
clientCrypto = client.crypto(),
)
}

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
/**
* Models result of initializing cryptography functionality for the Bitwarden SDK.
*/
sealed class InitializeCryptoResult {
/**
* Successfully initialized cryptography functionality.
*/
data object Success : InitializeCryptoResult()
/**
* Incorrect password provided.
*/
data object AuthenticationError : InitializeCryptoResult()
}

View File

@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.vault.repository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
/**
* Responsible for managing vault data inside the network layer.
*/
@@ -8,5 +10,10 @@ interface VaultRepository {
/**
* Attempt to sync the vault data.
*/
suspend fun sync()
fun sync()
/**
* Attempt to initialize crypto and sync the vault data.
*/
suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult
}

View File

@@ -1,11 +1,15 @@
package com.x8bit.bitwarden.data.vault.repository
import com.bitwarden.core.InitCryptoRequest
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -24,7 +28,7 @@ class VaultRepositoryImpl constructor(
private var syncJob: Job = Job().apply { complete() }
override suspend fun sync() {
override fun sync() {
if (!syncJob.isCompleted) return
syncJob = scope.launch {
syncService
@@ -36,7 +40,6 @@ class VaultRepositoryImpl constructor(
privateKey = syncResponse.profile?.privateKey,
)
// TODO transform into domain object consumable by VaultViewModel BIT-205.
// TODO initialize crypto in BIT-990
syncResponse.ciphers?.let { networkCiphers ->
vaultSdkSource.decryptCipherList(
cipherList = networkCiphers.toEncryptedSdkCipherList(),
@@ -55,6 +58,13 @@ class VaultRepositoryImpl constructor(
}
}
override suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult {
return initializeCrypto(masterPassword = masterPassword)
.also { vaultUnlockedResult ->
if (vaultUnlockedResult is VaultUnlockResult.Success) sync()
}
}
private fun storeUserKeyAndPrivateKey(
userKey: String?,
privateKey: String?,
@@ -72,4 +82,30 @@ class VaultRepositoryImpl constructor(
)
}
}
@Suppress("ReturnCount")
private suspend fun initializeCrypto(masterPassword: String): VaultUnlockResult {
val userState = authDiskSource.userState
?: return VaultUnlockResult.InvalidStateError
val userKey = authDiskSource.getUserKey(userId = userState.activeUserId)
?: return VaultUnlockResult.InvalidStateError
val privateKey = authDiskSource.getPrivateKey(userId = userState.activeUserId)
?: return VaultUnlockResult.InvalidStateError
return vaultSdkSource
.initializeCrypto(
request = InitCryptoRequest(
kdfParams = userState.activeAccount.profile.toSdkParams(),
email = userState.activeAccount.profile.email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
// TODO use actual organization keys BIT-1091
organizationKeys = mapOf(),
),
)
.fold(
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { it.toVaultUnlockResult() },
)
}
}

View File

@@ -17,7 +17,7 @@ import javax.inject.Singleton
*/
@Module
@InstallIn(SingletonComponent::class)
class VaultRepositoryModule {
object VaultRepositoryModule {
@Provides
@Singleton

View File

@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models result of unlocking the vault.
*/
sealed class VaultUnlockResult {
/**
* Vault successfully unlocked.
*/
data object Success : VaultUnlockResult()
/**
* Incorrect password provided.
*/
data object AuthenticationError : VaultUnlockResult()
/**
* Unable to access user state information.
*/
data object InvalidStateError : VaultUnlockResult()
/**
* Generic error thrown by Bitwarden SDK.
*/
data object GenericError : VaultUnlockResult()
}

View File

@@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.vault.repository.util
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
/**
* Transform a [InitializeCryptoResult] to [VaultUnlockResult].
*/
fun InitializeCryptoResult.toVaultUnlockResult(): VaultUnlockResult =
when (this) {
InitializeCryptoResult.AuthenticationError -> VaultUnlockResult.AuthenticationError
InitializeCryptoResult.Success -> VaultUnlockResult.Success
}