Add storage for biometrics key (#798)

This commit is contained in:
David Perez 2024-01-26 00:22:07 -06:00 committed by Álison Fernandes
parent f2d90dda55
commit 3264be998d
5 changed files with 92 additions and 2 deletions

View file

@ -100,6 +100,16 @@ interface AuthDiskSource {
*/
fun storeUserAutoUnlockKey(userId: String, userAutoUnlockKey: String?)
/**
* Gets the biometrics key for the given [userId].
*/
fun getUserBiometricUnlockKey(userId: String): String?
/**
* Stores the biometrics key for the given [userId].
*/
fun storeUserBiometricUnlockKey(userId: String, biometricsKey: String?)
/**
* Retrieves a pin-protected user key for the given [userId].
*/

View file

@ -15,6 +15,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID
private const val BIOMETRICS_UNLOCK_KEY = "$ENCRYPTED_BASE_KEY:userKeyBiometricUnlock"
private const val USER_AUTO_UNLOCK_KEY_KEY = "$ENCRYPTED_BASE_KEY:userKeyAutoUnlock"
private const val UNIQUE_APP_ID_KEY = "$BASE_KEY:appId"
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
@ -92,6 +93,7 @@ class AuthDiskSourceImpl(
storePrivateKey(userId = userId, privateKey = null)
storeOrganizationKeys(userId = userId, organizationKeys = null)
storeOrganizations(userId = userId, organizations = null)
storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
}
override fun getLastActiveTimeMillis(userId: String): Long? =
@ -156,6 +158,19 @@ class AuthDiskSourceImpl(
)
}
override fun getUserBiometricUnlockKey(userId: String): String? =
getEncryptedString(key = "${BIOMETRICS_UNLOCK_KEY}_$userId")
override fun storeUserBiometricUnlockKey(
userId: String,
biometricsKey: String?,
) {
putEncryptedString(
key = "${BIOMETRICS_UNLOCK_KEY}_$userId",
value = biometricsKey,
)
}
override fun getPinProtectedUserKey(userId: String): String? =
inMemoryPinProtectedUserKeys[userId]
?: getString(key = "${PIN_PROTECTED_USER_KEY_KEY}_$userId")

View file

@ -27,10 +27,11 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@Suppress("LargeClass")
class AuthDiskSourceTest {
private val fakeEncryptedSharedPreferences = FakeSharedPreferences()
private val fakeSharedPreferences = FakeSharedPreferences()
private val legacySecureStorageMigrator = mockk<LegacySecureStorageMigrator>() {
private val legacySecureStorageMigrator = mockk<LegacySecureStorageMigrator> {
every { migrateIfNecessary() } just runs
}
@ -137,6 +138,10 @@ class AuthDiskSourceTest {
fun `clearData should clear all necessary data for the given user`() {
val userId = "userId"
authDiskSource.storeUserBiometricUnlockKey(
userId = userId,
biometricsKey = "1234-9876-0192",
)
authDiskSource.storeLastActiveTimeMillis(
userId = userId,
lastActiveTimeMillis = 123456789L,
@ -162,6 +167,7 @@ class AuthDiskSourceTest {
authDiskSource.clearData(userId = userId)
assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId))
assertNull(authDiskSource.getLastActiveTimeMillis(userId = userId))
assertNull(authDiskSource.getInvalidUnlockAttempts(userId = userId))
assertNull(authDiskSource.getUserKey(userId = userId))
@ -439,6 +445,52 @@ class AuthDiskSourceTest {
)
}
@Test
fun `getUserBiometricUnlockKey should pull from SharedPreferences`() {
val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock"
val mockUserId = "mockUserId"
val biometricsKeyKey = "${biometricsKeyBaseKey}_$mockUserId"
val biometricsKey = "1234"
fakeEncryptedSharedPreferences.edit {
putString(biometricsKeyKey, biometricsKey)
}
val actual = authDiskSource.getUserBiometricUnlockKey(userId = mockUserId)
assertEquals(biometricsKey, actual)
}
@Test
fun `storeUserBiometricUnlockKey for non-null values should update SharedPreferences`() {
val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock"
val mockUserId = "mockUserId"
val biometricsKeyKey = "${biometricsKeyBaseKey}_$mockUserId"
val biometricsKey = "1234"
authDiskSource.storeUserBiometricUnlockKey(
userId = mockUserId,
biometricsKey = biometricsKey,
)
val actual = fakeEncryptedSharedPreferences.getString(
key = biometricsKeyKey,
defaultValue = null,
)
assertEquals(biometricsKey, actual)
}
@Test
fun `storeUserBiometricUnlockKey for null values should clear SharedPreferences`() {
val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock"
val mockUserId = "mockUserId"
val biometricsKeyKey = "${biometricsKeyBaseKey}_$mockUserId"
val biometricsKey = "1234"
fakeEncryptedSharedPreferences.edit {
putString(biometricsKeyKey, biometricsKey)
}
authDiskSource.storeUserBiometricUnlockKey(
userId = mockUserId,
biometricsKey = null,
)
assertFalse(fakeEncryptedSharedPreferences.contains(biometricsKeyKey))
}
@Test
fun `getPinProtectedUserKey should pull from SharedPreferences`() {
val pinProtectedUserKeyBaseKey = "bwPreferencesStorage:pinKeyEncryptedUserKey"

View file

@ -30,6 +30,7 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedOrganizations =
mutableMapOf<String, List<SyncResponseJson.Profile.Organization>?>()
private val storedOrganizationKeys = mutableMapOf<String, Map<String, String>?>()
private val storedBiometricKeys = mutableMapOf<String, String?>()
override var userState: UserStateJson? = null
set(value) {
@ -50,6 +51,7 @@ class FakeAuthDiskSource : AuthDiskSource {
storedPinProtectedUserKeys.remove(userId)
storedEncryptedPins.remove(userId)
storedOrganizations.remove(userId)
storedBiometricKeys.remove(userId)
storedOrganizationKeys.remove(userId)
mutableOrganizationsFlowMap.remove(userId)
@ -146,6 +148,13 @@ class FakeAuthDiskSource : AuthDiskSource {
getMutableOrganizationsFlow(userId = userId).tryEmit(organizations)
}
override fun getUserBiometricUnlockKey(userId: String): String? =
storedBiometricKeys[userId]
override fun storeUserBiometricUnlockKey(userId: String, biometricsKey: String?) {
storedBiometricKeys[userId] = biometricsKey
}
/**
* Assert that the given [userState] matches the currently tracked value.
*/

View file

@ -100,6 +100,10 @@ class FakeSharedPreferences : SharedPreferences {
private inline fun <reified T> putValue(
key: String,
value: T,
): SharedPreferences.Editor = apply { pendingSharedPreferences[key] = value }
): SharedPreferences.Editor = apply {
value
?.let { pendingSharedPreferences[key] = it }
?: pendingSharedPreferences.remove(key)
}
}
}