mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 09:25:58 +03:00
Upon trusting device, update decryption options (#1211)
This commit is contained in:
parent
663c9785cf
commit
e17176f934
4 changed files with 308 additions and 18 deletions
|
@ -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 }
|
||||
|
|
|
@ -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) },
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
)
|
Loading…
Reference in a new issue