Upon trusting device, update decryption options (#1211)

This commit is contained in:
David Perez 2024-04-02 17:21:50 -05:00 committed by Álison Fernandes
parent 663c9785cf
commit e17176f934
4 changed files with 308 additions and 18 deletions

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.manager.util.toUserStateJson
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
@ -44,6 +45,10 @@ class TrustedDeviceManagerImpl(
userId = userId,
deviceKey = trustedDevice.deviceKey,
)
authDiskSource.userState = trustedDevice.toUserStateJson(
userId = userId,
previousUserState = requireNotNull(authDiskSource.userState),
)
}
}
.also { authDiskSource.shouldTrustDevice = false }

View file

@ -0,0 +1,58 @@
package com.x8bit.bitwarden.data.auth.manager.util
import com.bitwarden.crypto.TrustDeviceResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
/**
* Converts the given [TrustDeviceResponse] to an updated [UserStateJson], given the following
* additional information:
*
* - the [userId]
* - the [previousUserState]
*/
fun TrustDeviceResponse.toUserStateJson(
userId: String,
previousUserState: UserStateJson,
): UserStateJson {
val trustedAccount = requireNotNull(previousUserState.accounts[userId])
val profile = trustedAccount.profile
// The UserDecryptionOptionsJson and TrustedDeviceUserDecryptionOptionsJson
// should be present at this time, but we have fallbacks just in case.
val decryptionOptions = profile
.userDecryptionOptions
?: UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = null,
keyConnectorUserDecryptionOptions = null,
)
val deviceOptions = decryptionOptions
.trustedDeviceUserDecryptionOptions
?.copy(
encryptedPrivateKey = this.protectedDevicePrivateKey,
encryptedUserKey = this.protectedUserKey,
)
?: TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = this.protectedDevicePrivateKey,
encryptedUserKey = this.protectedUserKey,
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)
val account = trustedAccount.copy(
profile = profile.copy(
userDecryptionOptions = decryptionOptions.copy(
trustedDeviceUserDecryptionOptions = deviceOptions,
),
),
)
// Update the existing UserState.
return previousUserState.copy(
accounts = previousUserState
.accounts
.toMutableMap()
.apply { put(userId, account) },
)
}

View file

@ -1,18 +1,30 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.crypto.TrustDeviceResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
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.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.manager.util.toUserStateJson
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
@ -27,11 +39,20 @@ class TrustedDeviceManagerTests {
devicesService = devicesService,
)
@BeforeEach
fun setup() {
mockkStatic(TrustDeviceResponse::toUserStateJson)
}
@AfterEach
fun tearDown() {
unmockkStatic(TrustDeviceResponse::toUserStateJson)
}
@Suppress("MaxLineLength")
@Test
fun `trustThisDeviceIfNecessary when shouldTrustDevice false should return success with false`() =
runTest {
val userId = "userId"
val deviceKey = "deviceKey"
val trustedDeviceResponse = TrustDeviceResponse(
deviceKey = deviceKey,
@ -41,19 +62,19 @@ class TrustedDeviceManagerTests {
)
fakeAuthDiskSource.shouldTrustDevice = false
coEvery {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
} returns trustedDeviceResponse.asSuccess()
val result = manager.trustThisDeviceIfNecessary(userId = userId)
val result = manager.trustThisDeviceIfNecessary(userId = USER_ID)
assertEquals(false.asSuccess(), result)
fakeAuthDiskSource.assertDeviceKey(
userId = userId,
userId = USER_ID,
deviceKey = deviceKey,
inMemoryOnly = true,
)
coVerify(exactly = 1) {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
}
coVerify(exactly = 0) {
devicesService.trustDevice(
@ -67,19 +88,18 @@ class TrustedDeviceManagerTests {
@Test
fun `trustThisDeviceIfNecessary when getTrustDevice fails should return failure`() = runTest {
val userId = "userId"
fakeAuthDiskSource.shouldTrustDevice = true
val error = Throwable("Fail")
coEvery {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
} returns error.asFailure()
val result = manager.trustThisDeviceIfNecessary(userId = userId)
val result = manager.trustThisDeviceIfNecessary(userId = USER_ID)
assertEquals(error.asFailure(), result)
assertFalse(fakeAuthDiskSource.shouldTrustDevice)
coVerify(exactly = 1) {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
}
coVerify(exactly = 0) {
devicesService.trustDevice(
@ -93,7 +113,6 @@ class TrustedDeviceManagerTests {
@Test
fun `trustThisDeviceIfNecessary when trustDevice fails should return failure`() = runTest {
val userId = "userId"
val deviceKey = "deviceKey"
val protectedUserKey = "protectedUserKey"
val protectedDevicePrivateKey = "protectedDevicePrivateKey"
@ -107,7 +126,7 @@ class TrustedDeviceManagerTests {
val error = Throwable("Fail")
fakeAuthDiskSource.shouldTrustDevice = true
coEvery {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
} returns trustedDeviceResponse.asSuccess()
coEvery {
devicesService.trustDevice(
@ -118,12 +137,12 @@ class TrustedDeviceManagerTests {
)
} returns error.asFailure()
val result = manager.trustThisDeviceIfNecessary(userId = userId)
val result = manager.trustThisDeviceIfNecessary(userId = USER_ID)
assertEquals(error.asFailure(), result)
assertFalse(fakeAuthDiskSource.shouldTrustDevice)
coVerify(exactly = 1) {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
devicesService.trustDevice(
appId = "testUniqueAppId",
encryptedUserKey = protectedUserKey,
@ -135,7 +154,6 @@ class TrustedDeviceManagerTests {
@Test
fun `trustThisDeviceIfNecessary when success should return success with true`() = runTest {
val userId = "userId"
val deviceKey = "deviceKey"
val protectedUserKey = "protectedUserKey"
val protectedDevicePrivateKey = "protectedDevicePrivateKey"
@ -153,9 +171,10 @@ class TrustedDeviceManagerTests {
type = 0,
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
)
fakeAuthDiskSource.userState = DEFAULT_USER_STATE
fakeAuthDiskSource.shouldTrustDevice = true
coEvery {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
} returns trustedDeviceResponse.asSuccess()
coEvery {
devicesService.trustDevice(
@ -165,14 +184,21 @@ class TrustedDeviceManagerTests {
encryptedDevicePrivateKey = protectedDevicePrivateKey,
)
} returns trustedDeviceKeysResponseJson.asSuccess()
every {
trustedDeviceResponse.toUserStateJson(
userId = USER_ID,
previousUserState = DEFAULT_USER_STATE,
)
} returns UPDATED_USER_STATE
val result = manager.trustThisDeviceIfNecessary(userId = userId)
val result = manager.trustThisDeviceIfNecessary(userId = USER_ID)
assertEquals(true.asSuccess(), result)
fakeAuthDiskSource.assertDeviceKey(userId = userId, deviceKey = deviceKey)
fakeAuthDiskSource.assertDeviceKey(userId = USER_ID, deviceKey = deviceKey)
assertFalse(fakeAuthDiskSource.shouldTrustDevice)
fakeAuthDiskSource.assertUserState(UPDATED_USER_STATE)
coVerify(exactly = 1) {
vaultSdkSource.getTrustDevice(userId = userId)
vaultSdkSource.getTrustDevice(userId = USER_ID)
devicesService.trustDevice(
appId = "testUniqueAppId",
encryptedUserKey = protectedUserKey,
@ -182,3 +208,87 @@ class TrustedDeviceManagerTests {
}
}
}
private const val USER_ID: String = "userId"
private val DEFAULT_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = null,
encryptedUserKey = null,
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)
private val UPDATED_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = "encryptedPrivateKey",
encryptedUserKey = "encryptedUserKey",
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)
private val DEFAULT_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = DEFAULT_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS,
keyConnectorUserDecryptionOptions = null,
)
private val UPDATED_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = UPDATED_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS,
keyConnectorUserDecryptionOptions = null,
)
private val DEFAULT_ACCOUNT = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID,
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = DEFAULT_USER_DECRYPTION_OPTIONS,
),
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
),
)
private val UPDATED_ACCOUNT = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID,
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = UPDATED_USER_DECRYPTION_OPTIONS,
),
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
),
)
private val DEFAULT_USER_STATE = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(USER_ID to DEFAULT_ACCOUNT),
)
private val UPDATED_USER_STATE = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(USER_ID to UPDATED_ACCOUNT),
)

View file

@ -0,0 +1,117 @@
package com.x8bit.bitwarden.data.auth.manager.util
import com.bitwarden.crypto.TrustDeviceResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class TrustDeviceResponseExtensionsTest {
@Test
fun `toUserState updates the previous state`() {
assertEquals(
UPDATED_USER_STATE,
DEFAULT_TRUST_DEVICE_RESPONSE.toUserStateJson(
userId = USER_ID,
previousUserState = DEFAULT_USER_STATE,
),
)
}
}
private const val USER_ID: String = "userId"
private const val USER_KEY: String = "protectedUserKey"
private const val PRIVATE_KEY: String = "protectedDevicePrivateKey"
private val DEFAULT_TRUST_DEVICE_RESPONSE: TrustDeviceResponse = TrustDeviceResponse(
deviceKey = "deviceKey",
protectedUserKey = USER_KEY,
protectedDevicePrivateKey = PRIVATE_KEY,
protectedDevicePublicKey = "protectedDevicePublicKey",
)
private val DEFAULT_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = null,
encryptedUserKey = null,
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)
private val UPDATED_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS = TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = PRIVATE_KEY,
encryptedUserKey = USER_KEY,
hasAdminApproval = false,
hasLoginApprovingDevice = false,
hasManageResetPasswordPermission = false,
)
private val DEFAULT_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = DEFAULT_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS,
keyConnectorUserDecryptionOptions = null,
)
private val UPDATED_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = UPDATED_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS,
keyConnectorUserDecryptionOptions = null,
)
private val DEFAULT_ACCOUNT = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID,
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = DEFAULT_USER_DECRYPTION_OPTIONS,
),
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
),
)
private val UPDATED_ACCOUNT = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID,
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = UPDATED_USER_DECRYPTION_OPTIONS,
),
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
),
)
private val DEFAULT_USER_STATE = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(USER_ID to DEFAULT_ACCOUNT),
)
private val UPDATED_USER_STATE = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(USER_ID to UPDATED_ACCOUNT),
)