mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 20:09:59 +03:00
Store organization keys during sync request (#367)
This commit is contained in:
parent
436c567b3f
commit
03c0da4917
8 changed files with 194 additions and 19 deletions
|
@ -48,4 +48,19 @@ interface AuthDiskSource {
|
|||
* Stores a private key using a [userId].
|
||||
*/
|
||||
fun storePrivateKey(userId: String, privateKey: String?)
|
||||
|
||||
/**
|
||||
* Gets the organization keys for the given [userId] in the form of a mapping from organization
|
||||
* ID to encrypted organization key.
|
||||
*/
|
||||
fun getOrganizationKeys(userId: String): Map<String, String>?
|
||||
|
||||
/**
|
||||
* Stores the organization keys for the given [userId] in the form of a mapping from
|
||||
* organization ID to encrypted organization key.
|
||||
*/
|
||||
fun storeOrganizationKeys(
|
||||
userId: String,
|
||||
organizationKeys: Map<String, String>?,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
|
|||
private const val STATE_KEY = "$BASE_KEY:state"
|
||||
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "$BASE_KEY:masterKeyEncryptedUserKey"
|
||||
private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "$BASE_KEY:encPrivateKey"
|
||||
private const val ORGANIZATION_KEYS_KEY = "$BASE_KEY:encOrgKeys"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
|
@ -76,6 +77,20 @@ class AuthDiskSourceImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getOrganizationKeys(userId: String): Map<String, String>? =
|
||||
getString(key = "${ORGANIZATION_KEYS_KEY}_$userId")
|
||||
?.let { json.decodeFromString(it) }
|
||||
|
||||
override fun storeOrganizationKeys(
|
||||
userId: String,
|
||||
organizationKeys: Map<String, String>?,
|
||||
) {
|
||||
putString(
|
||||
key = "${ORGANIZATION_KEYS_KEY}_$userId",
|
||||
value = organizationKeys?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
|
|
@ -230,8 +230,12 @@ class AuthRepositoryImpl constructor(
|
|||
val updatedAccounts = currentUserState
|
||||
.accounts
|
||||
.filterKeys { it != userId }
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = null)
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = null)
|
||||
authDiskSource.apply {
|
||||
storeUserKey(userId = userId, userKey = null)
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeOrganizationKeys(userId = userId, organizationKeys = null)
|
||||
}
|
||||
|
||||
// Check if there is a new active user
|
||||
if (updatedAccounts.isNotEmpty()) {
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
|
|
|
@ -107,10 +107,7 @@ class VaultRepositoryImpl constructor(
|
|||
syncResponse = syncResponse,
|
||||
)
|
||||
|
||||
storeUserKeyAndPrivateKey(
|
||||
userKey = syncResponse.profile?.key,
|
||||
privateKey = syncResponse.profile?.privateKey,
|
||||
)
|
||||
storeKeys(syncResponse = syncResponse)
|
||||
decryptSyncResponseAndUpdateVaultDataState(syncResponse = syncResponse)
|
||||
decryptSendsAndUpdateSendDataState(sendList = syncResponse.sends)
|
||||
},
|
||||
|
@ -297,12 +294,13 @@ class VaultRepositoryImpl constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun storeUserKeyAndPrivateKey(
|
||||
userKey: String?,
|
||||
privateKey: String?,
|
||||
private fun storeKeys(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val userId = authDiskSource.userState?.activeUserId ?: return
|
||||
if (userKey == null || privateKey == null) return
|
||||
val profile = syncResponse.profile ?: return
|
||||
val userId = profile.id
|
||||
val userKey = profile.key
|
||||
val privateKey = profile.privateKey
|
||||
authDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = userId,
|
||||
|
@ -312,6 +310,13 @@ class VaultRepositoryImpl constructor(
|
|||
userId = userId,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = profile.id,
|
||||
organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -192,6 +192,64 @@ class AuthDiskSourceTest {
|
|||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOrganizationKeys should pull from SharedPreferences`() {
|
||||
val organizationKeysBaseKey = "bwPreferencesStorage:encOrgKeys"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockOrganizationKeys = mapOf(
|
||||
"organizationId1" to "organizationKey1",
|
||||
"organizationId2" to "organizationKey2",
|
||||
)
|
||||
fakeSharedPreferences
|
||||
.edit()
|
||||
.putString(
|
||||
"${organizationKeysBaseKey}_$mockUserId",
|
||||
"""
|
||||
{
|
||||
"organizationId1": "organizationKey1",
|
||||
"organizationId2": "organizationKey2"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
.apply()
|
||||
val actual = authDiskSource.getOrganizationKeys(userId = mockUserId)
|
||||
assertEquals(
|
||||
mockOrganizationKeys,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `putOrganizationKeys should update SharedPreferences`() {
|
||||
val organizationKeysBaseKey = "bwPreferencesStorage:encOrgKeys"
|
||||
val mockUserId = "mockUserId"
|
||||
val mockOrganizationKeys = mapOf(
|
||||
"organizationId1" to "organizationKey1",
|
||||
"organizationId2" to "organizationKey2",
|
||||
)
|
||||
authDiskSource.storeOrganizationKeys(
|
||||
userId = mockUserId,
|
||||
organizationKeys = mockOrganizationKeys,
|
||||
)
|
||||
val actual = fakeSharedPreferences.getString(
|
||||
"${organizationKeysBaseKey}_$mockUserId",
|
||||
null,
|
||||
)
|
||||
assertEquals(
|
||||
json.parseToJsonElement(
|
||||
"""
|
||||
{
|
||||
"organizationId1": "organizationKey1",
|
||||
"organizationId2": "organizationKey2"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
),
|
||||
json.parseToJsonElement(requireNotNull(actual)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_STATE_JSON = """
|
||||
|
|
|
@ -8,10 +8,21 @@ import kotlinx.coroutines.flow.onSubscription
|
|||
import org.junit.Assert.assertEquals
|
||||
|
||||
class FakeAuthDiskSource : AuthDiskSource {
|
||||
|
||||
override val uniqueAppId: String = "testUniqueAppId"
|
||||
|
||||
override var rememberedEmailAddress: String? = null
|
||||
|
||||
private val mutableUserStateFlow =
|
||||
MutableSharedFlow<UserStateJson?>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
private val storedUserKeys = mutableMapOf<String, String?>()
|
||||
private val storedPrivateKeys = mutableMapOf<String, String?>()
|
||||
private val storedOrganizationKeys = mutableMapOf<String, Map<String, String>?>()
|
||||
|
||||
override var userState: UserStateJson? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
@ -33,15 +44,16 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
storedPrivateKeys[userId] = privateKey
|
||||
}
|
||||
|
||||
private val mutableUserStateFlow =
|
||||
MutableSharedFlow<UserStateJson?>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
override fun getOrganizationKeys(
|
||||
userId: String,
|
||||
): Map<String, String>? = storedOrganizationKeys[userId]
|
||||
|
||||
private val storedUserKeys = mutableMapOf<String, String?>()
|
||||
|
||||
private val storedPrivateKeys = mutableMapOf<String, String?>()
|
||||
override fun storeOrganizationKeys(
|
||||
userId: String,
|
||||
organizationKeys: Map<String, String>?,
|
||||
) {
|
||||
storedOrganizationKeys[userId] = organizationKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given [userState] matches the currently tracked value.
|
||||
|
@ -63,4 +75,11 @@ class FakeAuthDiskSource : AuthDiskSource {
|
|||
fun assertPrivateKey(userId: String, privateKey: String?) {
|
||||
assertEquals(privateKey, storedPrivateKeys[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the the [organizationKeys] was stored successfully using the [userId].
|
||||
*/
|
||||
fun assertOrganizationKeys(userId: String, organizationKeys: Map<String, String>?) {
|
||||
assertEquals(organizationKeys, storedOrganizationKeys[userId])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -957,6 +957,20 @@ class AuthRepositoryTest {
|
|||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = PUBLIC_KEY,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = PRIVATE_KEY,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = USER_ID_1,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
)
|
||||
}
|
||||
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
|
||||
|
@ -979,6 +993,10 @@ class AuthRepositoryTest {
|
|||
userId = USER_ID_1,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = USER_ID_1,
|
||||
organizationKeys = null,
|
||||
)
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
|
@ -1021,6 +1039,20 @@ class AuthRepositoryTest {
|
|||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns MULTI_USER_STATE
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = USER_ID_2,
|
||||
userKey = PUBLIC_KEY,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = USER_ID_2,
|
||||
privateKey = PRIVATE_KEY,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = USER_ID_2,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
)
|
||||
}
|
||||
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
|
||||
|
@ -1043,6 +1075,10 @@ class AuthRepositoryTest {
|
|||
userId = USER_ID_1,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = USER_ID_1,
|
||||
organizationKeys = null,
|
||||
)
|
||||
verify { vaultRepository.clearUnlockedData() }
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
|
@ -1056,6 +1092,20 @@ class AuthRepositoryTest {
|
|||
accounts = initialUserState.accounts.filter { it.key != USER_ID_2 },
|
||||
)
|
||||
fakeAuthDiskSource.userState = initialUserState
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserKey(
|
||||
userId = USER_ID_2,
|
||||
userKey = PUBLIC_KEY,
|
||||
)
|
||||
storePrivateKey(
|
||||
userId = USER_ID_2,
|
||||
privateKey = PRIVATE_KEY,
|
||||
)
|
||||
storeOrganizationKeys(
|
||||
userId = USER_ID_2,
|
||||
organizationKeys = ORGANIZATION_KEYS,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(initialUserState, fakeAuthDiskSource.userState)
|
||||
|
||||
|
@ -1075,6 +1125,10 @@ class AuthRepositoryTest {
|
|||
userId = USER_ID_2,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = USER_ID_2,
|
||||
organizationKeys = null,
|
||||
)
|
||||
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
|
||||
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_2) }
|
||||
}
|
||||
|
@ -1295,6 +1349,7 @@ class AuthRepositoryTest {
|
|||
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
|
||||
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
|
||||
private const val USER_ID_3 = "3816ef34-0747-4133-9b7a-ba35d3768a68"
|
||||
private val ORGANIZATION_KEYS = mapOf("organizationId1" to "organizationKey1")
|
||||
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
|
||||
kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u),
|
||||
)
|
||||
|
|
|
@ -106,6 +106,10 @@ class VaultRepositoryTest {
|
|||
userId = "mockId-1",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.assertOrganizationKeys(
|
||||
userId = "mockId-1",
|
||||
organizationKeys = mapOf("mockId-1" to "mockKey-1"),
|
||||
)
|
||||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
|
|
Loading…
Add table
Reference in a new issue