mirror of
https://github.com/bitwarden/android.git
synced 2024-11-26 19:36:18 +03:00
Vault repo biometrics (#821)
This commit is contained in:
parent
aacb955720
commit
2dde22f762
8 changed files with 314 additions and 6 deletions
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
|
@ -169,6 +170,17 @@ interface SettingsRepository {
|
|||
*/
|
||||
fun storePullToRefreshEnabled(isPullToRefreshEnabled: Boolean)
|
||||
|
||||
/**
|
||||
* Clears any previously stored encrypted user key used with biometrics for the current user.
|
||||
*/
|
||||
fun clearBiometricsKey()
|
||||
|
||||
/**
|
||||
* Stores the encrypted user key for biometrics, allowing it to be used to unlock the current
|
||||
* user's vault.
|
||||
*/
|
||||
suspend fun setupBiometricsKey(): BiometricsKeyResult
|
||||
|
||||
/**
|
||||
* Stores the given PIN, allowing it to be used to unlock the current user's vault.
|
||||
*
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
|||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
|
@ -349,6 +350,24 @@ class SettingsRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setupBiometricsKey(): BiometricsKeyResult {
|
||||
val userId = activeUserId ?: return BiometricsKeyResult.Error
|
||||
return vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
.onSuccess {
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = it)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { BiometricsKeyResult.Success },
|
||||
onFailure = { BiometricsKeyResult.Error },
|
||||
)
|
||||
}
|
||||
|
||||
override fun clearBiometricsKey() {
|
||||
val userId = activeUserId ?: return
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
}
|
||||
|
||||
override fun storeUnlockPin(
|
||||
pin: String,
|
||||
shouldRequireMasterPasswordOnRestart: Boolean,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository.model
|
||||
|
||||
/**
|
||||
* Models result of setting up a biometrics key.
|
||||
*/
|
||||
sealed class BiometricsKeyResult {
|
||||
/**
|
||||
* Biometrics key setup successfully.
|
||||
*/
|
||||
data object Success : BiometricsKeyResult()
|
||||
|
||||
/**
|
||||
* Generic error while setting up the biometrics key.
|
||||
*/
|
||||
data object Error : BiometricsKeyResult()
|
||||
}
|
|
@ -155,6 +155,11 @@ interface VaultRepository : VaultLockManager {
|
|||
*/
|
||||
fun emitTotpCodeResult(totpCodeResult: TotpCodeResult)
|
||||
|
||||
/**
|
||||
* Attempt to unlock the vault using the stored biometric key for the currently active user.
|
||||
*/
|
||||
suspend fun unlockVaultWithBiometrics(): VaultUnlockResult
|
||||
|
||||
/**
|
||||
* Attempt to unlock the vault with the given [masterPassword] and for the currently active
|
||||
* user.
|
||||
|
|
|
@ -424,6 +424,25 @@ class VaultRepositoryImpl(
|
|||
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun unlockVaultWithBiometrics(): VaultUnlockResult {
|
||||
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
|
||||
val biometricsKey = authDiskSource
|
||||
.getUserBiometricUnlockKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError
|
||||
return unlockVaultForUser(
|
||||
userId = userId,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = biometricsKey,
|
||||
),
|
||||
)
|
||||
.also {
|
||||
if (it is VaultUnlockResult.Success) {
|
||||
deriveTemporaryPinProtectedUserKeyIfNecessary(userId = userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun unlockVaultWithMasterPassword(
|
||||
masterPassword: String,
|
||||
|
|
|
@ -238,6 +238,13 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
assertEquals(organizationKeys, storedOrganizationKeys[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the [biometricsKey] was stored successfully using the [userId].
|
||||
*/
|
||||
fun assertBiometricsKey(userId: String, biometricsKey: String?) {
|
||||
assertEquals(biometricsKey, storedBiometricKeys[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the [passwordHash] was stored successfully using the [userId].
|
||||
*/
|
||||
|
|
|
@ -10,9 +10,11 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
|||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
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 com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
|
@ -653,6 +655,70 @@ class SettingsRepositoryTest {
|
|||
assertEquals(true, fakeSettingsDiskSource.getPullToRefreshEnabled(userId = userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearBiometricsKey should remove the stored biometrics key`() {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
settingsRepository.clearBiometricsKey()
|
||||
|
||||
fakeAuthDiskSource.assertBiometricsKey(
|
||||
userId = userId,
|
||||
biometricsKey = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setupBiometricsKey with missing user state should return BiometricsKeyResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = settingsRepository.setupBiometricsKey()
|
||||
|
||||
assertEquals(BiometricsKeyResult.Error, result)
|
||||
coVerify(exactly = 0) {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `setupBiometricsKey with getUserEncryptionKey failure should return BiometricsKeyResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
|
||||
val result = settingsRepository.setupBiometricsKey()
|
||||
|
||||
assertEquals(BiometricsKeyResult.Error, result)
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `setupBiometricsKey with getUserEncryptionKey success should return BiometricsKeyResult Success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val encryptedKey = "asdf1234"
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
} returns encryptedKey.asSuccess()
|
||||
|
||||
val result = settingsRepository.setupBiometricsKey()
|
||||
|
||||
assertEquals(BiometricsKeyResult.Success, result)
|
||||
fakeAuthDiskSource.assertBiometricsKey(userId = userId, biometricsKey = encryptedKey)
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `storeUnlockPin when the master password on restart is required should only save an encrypted PIN to disk`() {
|
||||
|
|
|
@ -669,7 +669,171 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithMasterPassword with missing user state should return InvalidStateError `() =
|
||||
fun `unlockVaultWithBiometrics with missing user state should return InvalidStateError`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertEquals(
|
||||
VaultState(
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
),
|
||||
vaultRepository.vaultStateFlow.value,
|
||||
)
|
||||
|
||||
val result = vaultRepository.unlockVaultWithBiometrics()
|
||||
|
||||
assertEquals(VaultUnlockResult.InvalidStateError, result)
|
||||
assertEquals(
|
||||
VaultState(
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
),
|
||||
vaultRepository.vaultStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithBiometrics with missing biometrics key should return InvalidStateError`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
VaultState(
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
),
|
||||
vaultRepository.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
fakeAuthDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
|
||||
val result = vaultRepository.unlockVaultWithBiometrics()
|
||||
|
||||
assertEquals(VaultUnlockResult.InvalidStateError, result)
|
||||
assertEquals(
|
||||
VaultState(
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
),
|
||||
vaultRepository.vaultStateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithBiometrics with VaultLockManager Success and no encrypted PIN should unlock for the current user and return Success`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val privateKey = "mockPrivateKey-1"
|
||||
val biometricsKey = "asdf1234"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = MOCK_PROFILE.toSdkParams(),
|
||||
email = "email",
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = biometricsKey,
|
||||
),
|
||||
organizationKeys = null,
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey)
|
||||
storePrivateKey(userId = userId, privateKey = privateKey)
|
||||
}
|
||||
|
||||
val result = vaultRepository.unlockVaultWithBiometrics()
|
||||
|
||||
assertEquals(VaultUnlockResult.Success, result)
|
||||
coVerify {
|
||||
vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = MOCK_PROFILE.toSdkParams(),
|
||||
email = "email",
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = biometricsKey,
|
||||
),
|
||||
organizationKeys = null,
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultSdkSource.derivePinProtectedUserKey(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithBiometrics with VaultLockManager Success and a stored encrypted pin should unlock for the current user, derive a new pin-protected key, and return Success`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val encryptedPin = "encryptedPin"
|
||||
val privateKey = "mockPrivateKey-1"
|
||||
val pinProtectedUserKey = "pinProtectedUserkey"
|
||||
val biometricsKey = "asdf1234"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultSdkSource.derivePinProtectedUserKey(
|
||||
userId = userId,
|
||||
encryptedPin = encryptedPin,
|
||||
)
|
||||
} returns pinProtectedUserKey.asSuccess()
|
||||
coEvery {
|
||||
vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = MOCK_PROFILE.toSdkParams(),
|
||||
email = "email",
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = biometricsKey,
|
||||
),
|
||||
organizationKeys = null,
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey)
|
||||
storePrivateKey(userId = userId, privateKey = privateKey)
|
||||
storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
|
||||
storePinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = null,
|
||||
inMemoryOnly = true,
|
||||
)
|
||||
}
|
||||
|
||||
val result = vaultRepository.unlockVaultWithBiometrics()
|
||||
|
||||
assertEquals(VaultUnlockResult.Success, result)
|
||||
fakeAuthDiskSource.assertPinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
inMemoryOnly = true,
|
||||
)
|
||||
coVerify {
|
||||
vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = MOCK_PROFILE.toSdkParams(),
|
||||
email = "email",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = biometricsKey,
|
||||
),
|
||||
organizationKeys = null,
|
||||
)
|
||||
}
|
||||
coEvery {
|
||||
vaultSdkSource.derivePinProtectedUserKey(
|
||||
userId = userId,
|
||||
encryptedPin = encryptedPin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithMasterPassword with missing user state should return InvalidStateError`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertEquals(
|
||||
|
@ -697,7 +861,7 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithMasterPassword with missing user key should return InvalidStateError `() =
|
||||
fun `unlockVaultWithMasterPassword with missing user key should return InvalidStateError`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
VaultState(
|
||||
|
@ -732,7 +896,7 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithMasterPassword with missing private key should return InvalidStateError `() =
|
||||
fun `unlockVaultWithMasterPassword with missing private key should return InvalidStateError`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
VaultState(
|
||||
|
@ -907,7 +1071,7 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithPin with missing user state should return InvalidStateError `() =
|
||||
fun `unlockVaultWithPin with missing user state should return InvalidStateError`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertEquals(
|
||||
|
@ -935,7 +1099,7 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithPin with missing pin-protected user key should return InvalidStateError `() =
|
||||
fun `unlockVaultWithPin with missing pin-protected user key should return InvalidStateError`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
VaultState(
|
||||
|
@ -970,7 +1134,7 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithPin with missing private key should return InvalidStateError `() =
|
||||
fun `unlockVaultWithPin with missing private key should return InvalidStateError`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
VaultState(
|
||||
|
|
Loading…
Reference in a new issue