diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index 91dafdc2c..c964757ec 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -1,6 +1,7 @@ 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.PendingAuthRequestJson 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 @@ -121,6 +122,19 @@ interface AuthDiskSource { */ fun storeDeviceKey(userId: String, deviceKey: String?) + /** + * Gets the stored [PendingAuthRequestJson] for the given [userId]. + */ + fun getPendingAuthRequest(userId: String): PendingAuthRequestJson? + + /** + * Stores the [PendingAuthRequestJson] for the given [userId]. + */ + fun storePendingAuthRequest( + userId: String, + pendingAuthRequest: PendingAuthRequestJson?, + ) + /** * Gets the biometrics key for the given [userId]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 5c2a127e1..ecdc8859d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -2,6 +2,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.PendingAuthRequestJson 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 @@ -21,6 +22,7 @@ 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 DEVICE_KEY_KEY = "$ENCRYPTED_BASE_KEY:deviceKey" +private const val PENDING_ADMIN_AUTH_REQUEST_KEY = "$ENCRYPTED_BASE_KEY:pendingAdminAuthRequest" private const val UNIQUE_APP_ID_KEY = "$BASE_KEY:appId" private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail" @@ -125,6 +127,7 @@ class AuthDiskSourceImpl( storeOrganizationKeys(userId = userId, organizationKeys = null) storeOrganizations(userId = userId, organizations = null) storeDeviceKey(userId = userId, deviceKey = null) + storePendingAuthRequest(userId = userId, pendingAuthRequest = null) storeUserBiometricUnlockKey(userId = userId, biometricsKey = null) storeMasterPasswordHash(userId = userId, passwordHash = null) storePolicies(userId = userId, policies = null) @@ -201,6 +204,22 @@ class AuthDiskSourceImpl( putEncryptedString(key = "${DEVICE_KEY_KEY}_$userId", value = deviceKey) } + override fun getPendingAuthRequest( + userId: String, + ): PendingAuthRequestJson? = + getEncryptedString(key = "${PENDING_ADMIN_AUTH_REQUEST_KEY}_$userId") + ?.let { json.decodeFromStringOrNull(it) } + + override fun storePendingAuthRequest( + userId: String, + pendingAuthRequest: PendingAuthRequestJson?, + ) { + putEncryptedString( + key = "${PENDING_ADMIN_AUTH_REQUEST_KEY}_$userId", + value = pendingAuthRequest?.let { json.encodeToString(it) }, + ) + } + override fun getUserBiometricUnlockKey(userId: String): String? = getEncryptedString(key = "${BIOMETRICS_UNLOCK_KEY}_$userId") diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt new file mode 100644 index 000000000..a2c1a3d2a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.auth.datasource.disk.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Container for the user's API tokens. + * + * @property requestId The ID of the pending Auth Request. + * @property requestPrivateKey The private of the pending Auth Request. + */ +@Serializable +data class PendingAuthRequestJson( + @SerialName("Id") + val requestId: String, + + @SerialName("PrivateKey") + val requestPrivateKey: String, +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 240b8d868..ccddc5d4a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson @@ -185,6 +186,13 @@ class AuthDiskSourceTest { userId = userId, deviceKey = "9876-5432-1234", ) + authDiskSource.storePendingAuthRequest( + userId = userId, + pendingAuthRequest = PendingAuthRequestJson( + requestId = "12345", + requestPrivateKey = "67890", + ), + ) authDiskSource.storeUserBiometricUnlockKey( userId = userId, biometricsKey = "1234-9876-0192", @@ -226,6 +234,7 @@ class AuthDiskSourceTest { authDiskSource.clearData(userId = userId) assertNull(authDiskSource.getDeviceKey(userId = userId)) + assertNull(authDiskSource.getPendingAuthRequest(userId = userId)) assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId)) assertNull(authDiskSource.getLastActiveTimeMillis(userId = userId)) assertNull(authDiskSource.getInvalidUnlockAttempts(userId = userId)) @@ -542,6 +551,60 @@ class AuthDiskSourceTest { assertFalse(fakeEncryptedSharedPreferences.contains(deviceKeyKey)) } + @Test + fun `getPendingAuthRequest should pull from SharedPreferences`() { + val pendingAdminAuthRequestBaseKey = "bwSecureStorage:pendingAdminAuthRequest" + val mockUserId = "mockUserId" + val pendingAdminAuthRequestKey = "${pendingAdminAuthRequestBaseKey}_$mockUserId" + fakeEncryptedSharedPreferences.edit { + putString( + pendingAdminAuthRequestKey, + """ + { + "Id": "12345", + "PrivateKey": "67890" + } + """, + ) + } + val actual = authDiskSource.getPendingAuthRequest(userId = mockUserId) + assertEquals( + PendingAuthRequestJson(requestId = "12345", requestPrivateKey = "67890"), + actual, + ) + } + + @Test + fun `storePendingAuthRequest for non-null values should update SharedPreferences`() { + val pendingAdminAuthRequestKeyBaseKey = "bwSecureStorage:pendingAdminAuthRequest" + val mockUserId = "mockUserId" + val pendingAuthRequestKey = "${pendingAdminAuthRequestKeyBaseKey}_$mockUserId" + val pendingAdminAuthRequest = PendingAuthRequestJson( + requestId = "12345", + requestPrivateKey = "67890", + ) + authDiskSource.storePendingAuthRequest( + userId = mockUserId, + pendingAuthRequest = pendingAdminAuthRequest, + ) + val actual = fakeEncryptedSharedPreferences.getString( + key = pendingAuthRequestKey, + defaultValue = null, + ) + assertEquals( + json.parseToJsonElement( + """ + { + "Id": "12345", + "PrivateKey": "67890" + } + """ + .trimIndent(), + ), + json.parseToJsonElement(requireNotNull(actual)), + ) + } + @Test fun `getUserBiometricUnlockKey should pull from SharedPreferences`() { val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt index 63b225fb9..fec9e1473 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -2,6 +2,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.PendingAuthRequestJson 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 @@ -38,6 +39,7 @@ class FakeAuthDiskSource : AuthDiskSource { private val storedOrganizationKeys = mutableMapOf?>() private val storedAccountTokens = mutableMapOf() private val storedDeviceKey = mutableMapOf() + private val storedPendingAuthRequests = mutableMapOf() private val storedBiometricKeys = mutableMapOf() private val storedMasterPasswordHashes = mutableMapOf() private val storedPolicies = mutableMapOf?>() @@ -65,6 +67,7 @@ class FakeAuthDiskSource : AuthDiskSource { storedOrganizations.remove(userId) storedPolicies.remove(userId) storedAccountTokens.remove(userId) + storedPendingAuthRequests.remove(userId) storedBiometricKeys.remove(userId) storedOrganizationKeys.remove(userId) @@ -170,6 +173,16 @@ class FakeAuthDiskSource : AuthDiskSource { storedDeviceKey[userId] = deviceKey } + override fun getPendingAuthRequest(userId: String): PendingAuthRequestJson? = + storedPendingAuthRequests[userId] + + override fun storePendingAuthRequest( + userId: String, + pendingAuthRequest: PendingAuthRequestJson?, + ) { + storedPendingAuthRequests[userId] = pendingAuthRequest + } + override fun getUserBiometricUnlockKey(userId: String): String? = storedBiometricKeys[userId] @@ -289,6 +302,13 @@ class FakeAuthDiskSource : AuthDiskSource { assertEquals(deviceKey, storedDeviceKey[userId]) } + /** + * Assert that the [pendingAuthRequest] was stored successfully using the [userId]. + */ + fun assertPendingAuthRequest(userId: String, pendingAuthRequest: PendingAuthRequestJson?) { + assertEquals(pendingAuthRequest, storedPendingAuthRequests[userId]) + } + /** * Assert that the [biometricsKey] was stored successfully using the [userId]. */