Add unlockVaultWithPinAndSync to VaultRepository (#613)

This commit is contained in:
Brian Yencho 2024-01-14 21:08:35 -06:00 committed by Álison Fernandes
parent 18af449e1f
commit 3e28e5cc9b
5 changed files with 375 additions and 202 deletions

View file

@ -108,9 +108,20 @@ interface VaultRepository : VaultLockManager {
fun emitTotpCodeResult(totpCodeResult: TotpCodeResult)
/**
* Attempt to unlock the vault and sync the vault data for the currently active user.
* Attempt to unlock the vault with the given [masterPassword] and syncs the vault data for the
* currently active user.
*/
suspend fun unlockVaultAndSyncForCurrentUser(masterPassword: String): VaultUnlockResult
suspend fun unlockVaultWithMasterPasswordAndSync(
masterPassword: String,
): VaultUnlockResult
/**
* Attempt to unlock the vault with the given [pin] and syncs the vault data for the currently
* active user.
*/
suspend fun unlockVaultWithPinAndSync(
pin: String,
): VaultUnlockResult
/**
* Attempt to unlock the vault with the specified user information.

View file

@ -284,7 +284,7 @@ class VaultRepositoryImpl(
}
@Suppress("ReturnCount")
override suspend fun unlockVaultAndSyncForCurrentUser(
override suspend fun unlockVaultWithMasterPasswordAndSync(
masterPassword: String,
): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
@ -304,6 +304,27 @@ class VaultRepositoryImpl(
}
}
@Suppress("ReturnCount")
override suspend fun unlockVaultWithPinAndSync(
pin: String,
): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
return unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Pin(
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
),
)
.also {
if (it is VaultUnlockResult.Success) {
sync()
}
}
}
override suspend fun unlockVault(
userId: String,
masterPassword: String,

View file

@ -127,7 +127,7 @@ class VaultUnlockViewModel @Inject constructor(
private fun handleUnlockClick() {
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
viewModelScope.launch {
val vaultUnlockResult = vaultRepo.unlockVaultAndSyncForCurrentUser(
val vaultUnlockResult = vaultRepo.unlockVaultWithMasterPasswordAndSync(
mutableStateFlow.value.passwordInput,
)
sendAction(VaultUnlockAction.Internal.ReceiveVaultUnlockResult(vaultUnlockResult))

View file

@ -553,61 +553,111 @@ class VaultRepositoryTest {
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with VaultLockManager Success should unlock for the current user, sync, and return Success`() =
fun `unlockVaultWithMasterPasswordAndSync with missing user state should return InvalidStateError `() =
runTest {
val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(
userId = userId,
sendList = listOf(createMockSdkSend(number = 1)),
)
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.userState = null
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(masterPassword = "")
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithMasterPasswordAndSync with missing user key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = null,
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithMasterPasswordAndSync with missing private key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = "mockKey-1",
)
fakeAuthDiskSource.storeOrganizationKeys(
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
organizationKeys = createMockOrganizationKeys(number = 1),
privateKey = null,
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithMasterPasswordAndSync with VaultLockManager Success should unlock for the current user, sync, and return Success`() =
runTest {
val userId = "mockId-1"
val mockVaultUnlockResult = VaultUnlockResult.Success
coEvery {
vaultLockManager.unlockVault(
userId = userId,
kdf = MOCK_PROFILE.toSdkParams(),
email = "email",
privateKey = "mockPrivateKey-1",
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = "mockPassword-1",
userKey = "mockKey-1",
),
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
organizationKeys = createMockOrganizationKeys(number = 1),
)
} returns mockVaultUnlockResult
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(
masterPassword = "mockPassword-1",
)
@ -634,61 +684,13 @@ class VaultRepositoryTest {
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with VaultLockManager non-Success should unlock for the current user and return the error`() =
fun `unlockVaultWithMasterPasswordAndSync with VaultLockManager non-Success should unlock for the current user and return the error`() =
runTest {
val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(
userId = userId,
sendList = listOf(createMockSdkSend(number = 1)),
)
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = "mockKey-1",
)
fakeAuthDiskSource.storeOrganizationKeys(
userId = "mockId-1",
organizationKeys = createMockOrganizationKeys(number = 1),
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockVaultUnlockResult = VaultUnlockResult.InvalidStateError
coEvery {
vaultLockManager.unlockVault(
userId = userId,
kdf = MOCK_PROFILE.toSdkParams(),
email = "email",
privateKey = "mockPrivateKey-1",
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = "mockPassword-1",
userKey = "mockKey-1",
),
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
organizationKeys = createMockOrganizationKeys(number = 1),
)
} returns mockVaultUnlockResult
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(
masterPassword = "mockPassword-1",
)
@ -713,6 +715,163 @@ class VaultRepositoryTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithPinAndSync with missing user state should return InvalidStateError `() =
runTest {
fakeAuthDiskSource.userState = null
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultWithPinAndSync(pin = "1234")
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithPinAndSync with missing pin-protected user key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultWithPinAndSync(pin = "1234")
fakeAuthDiskSource.storePinProtectedUserKey(
userId = "mockId-1",
pinProtectedUserKey = null,
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithPinAndSync with missing private key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultWithPinAndSync(pin = "1234")
fakeAuthDiskSource.storePinProtectedUserKey(
userId = "mockId-1",
pinProtectedUserKey = "mockKey-1",
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = null,
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithPinAndSync with VaultLockManager Success should unlock for the current user, sync, and return Success`() =
runTest {
val userId = "mockId-1"
val mockVaultUnlockResult = VaultUnlockResult.Success
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
val result = vaultRepository.unlockVaultWithPinAndSync(pin = "1234")
assertEquals(
mockVaultUnlockResult,
result,
)
coVerify { syncService.sync() }
coVerify {
vaultLockManager.unlockVault(
userId = userId,
kdf = MOCK_PROFILE.toSdkParams(),
email = "email",
privateKey = "mockPrivateKey-1",
initUserCryptoMethod = InitUserCryptoMethod.Pin(
pin = "1234",
pinProtectedUserKey = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultWithPinAndSync with VaultLockManager non-Success should unlock for the current user and return the error`() =
runTest {
val userId = "mockId-1"
val mockVaultUnlockResult = VaultUnlockResult.InvalidStateError
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
val result = vaultRepository.unlockVaultWithPinAndSync(pin = "1234")
assertEquals(
mockVaultUnlockResult,
result,
)
coVerify(exactly = 0) { syncService.sync() }
coVerify {
vaultLockManager.unlockVault(
userId = userId,
kdf = MOCK_PROFILE.toSdkParams(),
email = "email",
privateKey = "mockPrivateKey-1",
initUserCryptoMethod = InitUserCryptoMethod.Pin(
pin = "1234",
pinProtectedUserKey = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
}
}
@Test
fun `unlockVault should delegate to the VaultLockManager`() = runTest {
val userId = "userId"
@ -833,104 +992,6 @@ class VaultRepositoryTest {
coVerify(exactly = 1) { syncService.sync() }
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with missing user state should return InvalidStateError `() =
runTest {
fakeAuthDiskSource.userState = null
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with missing user key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = null,
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with missing private key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = "mockKey-1",
)
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = null,
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertEquals(
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Test
fun `clearUnlockedData should update the vaultDataStateFlow to Loading`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
@ -1704,6 +1765,86 @@ class VaultRepositoryTest {
//region Helper functions
/**
* Prepares for an unlock call with the given [unlockResult].
*/
private fun prepareStateForUnlocking(
unlockResult: VaultUnlockResult,
mockMasterPassword: String = "mockPassword-1",
mockPin: String = "1234",
) {
val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = userId,
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(
userId = userId,
sendList = listOf(createMockSdkSend(number = 1)),
)
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.storePrivateKey(
userId = userId,
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.storeUserKey(
userId = userId,
userKey = "mockKey-1",
)
fakeAuthDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = "mockKey-1",
)
fakeAuthDiskSource.storeOrganizationKeys(
userId = userId,
organizationKeys = createMockOrganizationKeys(number = 1),
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
// Master password unlock
coEvery {
vaultLockManager.unlockVault(
userId = userId,
kdf = MOCK_PROFILE.toSdkParams(),
email = "email",
privateKey = "mockPrivateKey-1",
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = mockMasterPassword,
userKey = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
} returns unlockResult
// PIN unlock
coEvery {
vaultLockManager.unlockVault(
userId = userId,
kdf = MOCK_PROFILE.toSdkParams(),
email = "email",
privateKey = "mockPrivateKey-1",
initUserCryptoMethod = InitUserCryptoMethod.Pin(
pin = mockPin,
pinProtectedUserKey = "mockKey-1",
),
organizationKeys = createMockOrganizationKeys(number = 1),
)
} returns unlockResult
}
/**
* Helper setup all flows required to properly subscribe to the
* [VaultRepository.vaultDataStateFlow].

View file

@ -252,7 +252,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
} returns VaultUnlockResult.AuthenticationError
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -265,7 +265,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
}
}
@ -275,7 +275,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
} returns VaultUnlockResult.GenericError
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -288,7 +288,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
}
}
@ -298,7 +298,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
} returns VaultUnlockResult.InvalidStateError
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -311,7 +311,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
}
}
@ -321,7 +321,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
} returns VaultUnlockResult.Success
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -330,7 +330,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
}
}