mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Add spot in encrypted shared preferences for tokens (#1038)
This commit is contained in:
parent
b14ed30af3
commit
946565ae54
4 changed files with 143 additions and 0 deletions
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -209,4 +210,19 @@ interface AuthDiskSource {
|
|||
* Stores the [policies] for the given [userId].
|
||||
*/
|
||||
fun storePolicies(userId: String, policies: List<SyncResponseJson.Policy>?)
|
||||
|
||||
/**
|
||||
* Gets the account tokens for the given [userId].
|
||||
*/
|
||||
fun getAccountTokens(userId: String): AccountTokensJson?
|
||||
|
||||
/**
|
||||
* Emits updates that track [getAccountTokens]. This will replay the last known value, if any.
|
||||
*/
|
||||
fun getAccountTokensFlow(userId: String): Flow<AccountTokensJson?>
|
||||
|
||||
/**
|
||||
* Stores the [accountTokens] for the given [userId].
|
||||
*/
|
||||
fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
|
||||
|
@ -16,6 +17,7 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import java.util.UUID
|
||||
|
||||
private const val ACCOUNT_TOKENS_KEY = "$ENCRYPTED_BASE_KEY:accountTokens"
|
||||
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"
|
||||
|
@ -60,6 +62,8 @@ class AuthDiskSourceImpl(
|
|||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||
private val mutablePoliciesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
|
||||
private val mutableAccountTokensFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
|
||||
|
||||
override val uniqueAppId: String
|
||||
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
|
||||
|
@ -111,6 +115,7 @@ class AuthDiskSourceImpl(
|
|||
storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
storeMasterPasswordHash(userId = userId, passwordHash = null)
|
||||
storePolicies(userId = userId, policies = null)
|
||||
storeAccountTokens(userId = userId, accountTokens = null)
|
||||
}
|
||||
|
||||
override fun getLastActiveTimeMillis(userId: String): Long? =
|
||||
|
@ -308,6 +313,22 @@ class AuthDiskSourceImpl(
|
|||
getMutablePoliciesFlow(userId = userId).tryEmit(policies)
|
||||
}
|
||||
|
||||
override fun getAccountTokens(userId: String): AccountTokensJson? =
|
||||
getEncryptedString(key = "${ACCOUNT_TOKENS_KEY}_$userId")
|
||||
?.let { json.decodeFromStringOrNull(it) }
|
||||
|
||||
override fun getAccountTokensFlow(userId: String): Flow<AccountTokensJson?> =
|
||||
getMutableAccountTokensFlow(userId = userId)
|
||||
.onSubscription { emit(getAccountTokens(userId = userId)) }
|
||||
|
||||
override fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?) {
|
||||
putEncryptedString(
|
||||
key = "${ACCOUNT_TOKENS_KEY}_$userId",
|
||||
value = accountTokens?.let { json.encodeToString(it) },
|
||||
)
|
||||
getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
@ -329,4 +350,11 @@ class AuthDiskSourceImpl(
|
|||
mutablePoliciesFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableAccountTokensFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<AccountTokensJson?> =
|
||||
mutableAccountTokensFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,6 +190,13 @@ class AuthDiskSourceTest {
|
|||
userId = userId,
|
||||
policies = listOf(createMockPolicy()),
|
||||
)
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
)
|
||||
|
||||
authDiskSource.clearData(userId = userId)
|
||||
|
||||
|
@ -202,6 +209,7 @@ class AuthDiskSourceTest {
|
|||
assertNull(authDiskSource.getOrganizationKeys(userId = userId))
|
||||
assertNull(authDiskSource.getOrganizations(userId = userId))
|
||||
assertNull(authDiskSource.getPolicies(userId = userId))
|
||||
assertNull(authDiskSource.getAccountTokens(userId = userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -826,6 +834,71 @@ class AuthDiskSourceTest {
|
|||
json.parseToJsonElement(requireNotNull(actual)),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccountTokens should pull from SharedPreferences`() {
|
||||
val baseKey = "bwSecureStorage:accountTokens"
|
||||
val mockUserId = "mockUserId"
|
||||
val accountTokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
)
|
||||
fakeEncryptedSharedPreferences.edit {
|
||||
putString("${baseKey}_$mockUserId", json.encodeToString(accountTokens))
|
||||
}
|
||||
val actual = authDiskSource.getAccountTokens(userId = mockUserId)
|
||||
assertEquals(accountTokens, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAccountTokensFlow should react to changes from storeAccountTokens`() = runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val accountTokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
)
|
||||
authDiskSource.getAccountTokensFlow(userId = mockUserId).test {
|
||||
// The initial values of the Flow and the property are in sync
|
||||
assertNull(authDiskSource.getAccountTokens(userId = mockUserId))
|
||||
assertNull(awaitItem())
|
||||
|
||||
// Updating the repository updates shared preferences
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = mockUserId,
|
||||
accountTokens = accountTokens,
|
||||
)
|
||||
assertEquals(accountTokens, awaitItem())
|
||||
|
||||
// clear the repository clears shared preferences
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = mockUserId,
|
||||
accountTokens = null,
|
||||
)
|
||||
assertNull(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeAccountTokens should update SharedPreferences`() {
|
||||
val baseKey = "bwSecureStorage:accountTokens"
|
||||
val mockUserId = "mockUserId"
|
||||
val accountTokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
)
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = mockUserId,
|
||||
accountTokens = accountTokens,
|
||||
)
|
||||
val actual = fakeEncryptedSharedPreferences.getString(
|
||||
key = "${baseKey}_$mockUserId",
|
||||
defaultValue = null,
|
||||
)
|
||||
assertEquals(
|
||||
json.encodeToJsonElement(accountTokens),
|
||||
json.parseToJsonElement(requireNotNull(actual)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_STATE_JSON = """
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.disk.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
|
@ -20,6 +21,8 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||
private val mutablePoliciesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
|
||||
private val mutableAccountTokensFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
|
||||
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
|
||||
|
||||
private val storedLastActiveTimeMillis = mutableMapOf<String, Long?>()
|
||||
|
@ -33,6 +36,7 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
private val storedOrganizations =
|
||||
mutableMapOf<String, List<SyncResponseJson.Profile.Organization>?>()
|
||||
private val storedOrganizationKeys = mutableMapOf<String, Map<String, String>?>()
|
||||
private val storedAccountTokens = mutableMapOf<String, AccountTokensJson?>()
|
||||
private val storedBiometricKeys = mutableMapOf<String, String?>()
|
||||
private val storedMasterPasswordHashes = mutableMapOf<String, String?>()
|
||||
private val storedPolicies = mutableMapOf<String, List<SyncResponseJson.Policy>?>()
|
||||
|
@ -57,10 +61,13 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
storedEncryptedPins.remove(userId)
|
||||
storedOrganizations.remove(userId)
|
||||
storedPolicies.remove(userId)
|
||||
storedAccountTokens.remove(userId)
|
||||
storedBiometricKeys.remove(userId)
|
||||
storedOrganizationKeys.remove(userId)
|
||||
|
||||
mutableOrganizationsFlowMap.remove(userId)
|
||||
mutablePoliciesFlowMap.remove(userId)
|
||||
mutableAccountTokensFlowMap.remove(userId)
|
||||
}
|
||||
|
||||
override fun getLastActiveTimeMillis(userId: String): Long? =
|
||||
|
@ -180,6 +187,18 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
getMutablePoliciesFlow(userId = userId).tryEmit(policies)
|
||||
}
|
||||
|
||||
override fun getAccountTokens(userId: String): AccountTokensJson? =
|
||||
storedAccountTokens[userId]
|
||||
|
||||
override fun getAccountTokensFlow(userId: String): Flow<AccountTokensJson?> =
|
||||
getMutableAccountTokensFlow(userId = userId)
|
||||
.onSubscription { emit(getAccountTokens(userId)) }
|
||||
|
||||
override fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?) {
|
||||
storedAccountTokens[userId] = accountTokens
|
||||
getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given [userState] matches the currently tracked value.
|
||||
*/
|
||||
|
@ -304,5 +323,12 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableAccountTokensFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<AccountTokensJson?> =
|
||||
mutableAccountTokensFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
//endregion Private helper functions
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue