Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
77f0577
SDK Update - com.bitwarden:sdk-android 3.0.0-8515-c5504676
bw-ghapp[bot] Aug 27, 2026
74a2c9f
SDK Update - com.bitwarden:sdk-android 3.0.0-8516-b41ba802
bw-ghapp[bot] Aug 27, 2026
fc08358
SDK Update - com.bitwarden:sdk-android 3.0.0-8521-e2dc275b
bw-ghapp[bot] Aug 27, 2026
23c40f5
SDK Update - com.bitwarden:sdk-android 3.0.0-8522-b19177ea
bw-ghapp[bot] Aug 27, 2026
452e787
SDK Update - com.bitwarden:sdk-android 3.0.0-8524-9daf108b
bw-ghapp[bot] Aug 27, 2026
14b1236
SDK Update - com.bitwarden:sdk-android 3.0.0-8540-db45e841
bw-ghapp[bot] Aug 28, 2026
fab8425
SDK Update - com.bitwarden:sdk-android 3.0.0-8541-b904fe46
bw-ghapp[bot] Aug 28, 2026
715f1b4
SDK Update - com.bitwarden:sdk-android 3.0.0-8560-2204cba3
bw-ghapp[bot] Aug 31, 2026
14bacc4
SDK Update - com.bitwarden:sdk-android 3.0.0-8564-58a8c615
bw-ghapp[bot] Aug 31, 2026
bace657
SDK Update - com.bitwarden:sdk-android 3.0.0-8567-12e6fe33
bw-ghapp[bot] Aug 31, 2026
d6a5e36
SDK Update - com.bitwarden:sdk-android 3.0.0-8571-cc0c3628
bw-ghapp[bot] Aug 31, 2026
aa092c4
SDK Update - com.bitwarden:sdk-android 3.0.0-8577-61a35d8c
bw-ghapp[bot] Aug 31, 2026
66d883f
SDK Update - com.bitwarden:sdk-android 3.0.0-8589-bdba9fa5
bw-ghapp[bot] Sep 1, 2026
90564da
SDK Update - com.bitwarden:sdk-android 3.0.0-8600-5afe6ec3
bw-ghapp[bot] Sep 1, 2026
8869787
SDK Update - com.bitwarden:sdk-android 3.0.0-8604-b4ffb809
bw-ghapp[bot] Sep 1, 2026
a5d2b63
SDK Update - com.bitwarden:sdk-android 3.0.0-8607-e132bb1e
bw-ghapp[bot] Sep 1, 2026
55678f6
SDK Update - com.bitwarden:sdk-android 3.0.0-8612-71a958f7
bw-ghapp[bot] Sep 1, 2026
26af875
SDK Update - com.bitwarden:sdk-android 3.0.0-8617-a399e434
bw-ghapp[bot] Sep 1, 2026
89e0721
SDK Update - com.bitwarden:sdk-android 3.0.0-8619-9ff60b6d
bw-ghapp[bot] Sep 1, 2026
a578d44
SDK Update - com.bitwarden:sdk-android 3.0.0-8632-e29b6bd1
bw-ghapp[bot] Sep 2, 2026
5baebae
SDK Update - com.bitwarden:sdk-android 3.0.0-8654-9f33ab58
bw-ghapp[bot] Sep 2, 2026
2bb9d8d
SDK Update - com.bitwarden:sdk-android 3.0.0-8661-7c0d2a08
bw-ghapp[bot] Sep 2, 2026
5ad1071
SDK Update - com.bitwarden:sdk-android 3.0.0-8662-f7733334
bw-ghapp[bot] Sep 2, 2026
312bbb6
SDK Update - com.bitwarden:sdk-android 3.0.0-8663-e91ca897
bw-ghapp[bot] Sep 2, 2026
81ac5e7
SDK Update - com.bitwarden:sdk-android 3.0.0-8671-5d8ae614
bw-ghapp[bot] Sep 3, 2026
20de9a2
[PM-39460] feat: Add v2 encrypted migrations grace period start to th…
bw-ghapp[bot] Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,23 @@ interface AuthDiskSource : AppIdProvider {
* Stores the v2 upgrade token for the given [userId].
*/
fun storeV2UpgradeToken(userId: String, v2UpgradeToken: V2UpgradeTokenJson?)

/**
* Gets the start of the v2 encrypted migrations grace period for the given [userId].
*
* This value is retained across logout and lock and is only removed when explicitly stored as
* `null`.
*/
fun getV2EncryptedMigrationsGracePeriodStart(userId: String): Instant?

/**
* Stores the start of the v2 encrypted migrations grace period for the given [userId].
*
* This value is retained across logout and lock and is only removed when explicitly stored as
* `null`.
*/
fun storeV2EncryptedMigrationsGracePeriodStart(
userId: String,
gracePeriodStart: Instant?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
private const val V2_UPGRADE_TOKEN = "v2UpgradeToken"
private const val USER_KEY_ID_KEY = "userKeyId"
private const val V2_ENCRYPTED_MIGRATIONS_GRACE_PERIOD_START =
"v2EncryptedMigrationsGracePeriodStart"

/**
* Primary implementation of [AuthDiskSource].
Expand Down Expand Up @@ -208,6 +210,7 @@ class AuthDiskSourceImpl(
// * DeviceKey
// * PendingAuthRequest
// * OnboardingStatus
// * V2EncryptedMigrationsGracePeriodStart
}

override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
Expand Down Expand Up @@ -632,6 +635,20 @@ class AuthDiskSourceImpl(
)
}

override fun getV2EncryptedMigrationsGracePeriodStart(userId: String): Instant? =
getLong(key = V2_ENCRYPTED_MIGRATIONS_GRACE_PERIOD_START.appendIdentifier(userId))
?.let { Instant.ofEpochMilli(it) }

override fun storeV2EncryptedMigrationsGracePeriodStart(
userId: String,
gracePeriodStart: Instant?,
) {
putLong(
key = V2_ENCRYPTED_MIGRATIONS_GRACE_PERIOD_START.appendIdentifier(userId),
value = gracePeriodStart?.toEpochMilli(),
)
}

private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.updateMasterPasswordUnlock
import com.x8bit.bitwarden.data.vault.repository.util.toSdkMasterPasswordUnlock
import com.x8bit.bitwarden.data.vault.repository.util.toV2UpgradeToken
import com.x8bit.bitwarden.data.vault.repository.util.toV2UpgradeTokenJson
import java.time.Instant

/**
* A user-scoped implementation of a Bitwarden SDK [StateBridgeForeignImpl].
Expand Down Expand Up @@ -167,6 +168,23 @@ internal class SdkStateBridge(
)
}

override suspend fun setV2EncryptedMigrationsGracePeriodStart(value: Instant) {
authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = userId,
gracePeriodStart = value,
)
}

override suspend fun getV2EncryptedMigrationsGracePeriodStart(): Instant? =
authDiskSource.getV2EncryptedMigrationsGracePeriodStart(userId = userId)

override suspend fun clearV2EncryptedMigrationsGracePeriodStart() {
authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = userId,
gracePeriodStart = null,
)
}

override suspend fun getWebauthnPrfUnlockData(): WebAuthnPrfUnlockData? {
// We do not support unlock with WebAuthn, so we can just return null
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,11 @@ class AuthDiskSourceTest {
),
)
authDiskSource.storeUserKeyId(userId = userId, userKeyId = "userKeyId")
val gracePeriodStart = Instant.parse("2025-01-13T12:00:00Z")
authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = userId,
gracePeriodStart = gracePeriodStart,
)

authDiskSource.clearData(userId = userId)

Expand All @@ -389,6 +394,10 @@ class AuthDiskSourceTest {
OnboardingStatus.AUTOFILL_SETUP,
authDiskSource.getOnboardingStatus(userId = userId),
)
assertEquals(
gracePeriodStart,
authDiskSource.getV2EncryptedMigrationsGracePeriodStart(userId = userId),
)

// These should be cleared
assertNull(authDiskSource.getUserBiometricInitVector(userId = userId))
Expand Down Expand Up @@ -1557,6 +1566,61 @@ class AuthDiskSourceTest {
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
assertNull(actual)
}

@Test
fun `getV2EncryptedMigrationsGracePeriodStart should pull from SharedPreferences`() {
val storeKey = "bwPreferencesStorage:v2EncryptedMigrationsGracePeriodStart"
val mockUserId = "mockUserId"
val expectedState = Instant.parse("2025-01-13T12:00:00Z")
fakeSharedPreferences.edit {
putLong("${storeKey}_$mockUserId", expectedState.toEpochMilli())
}

val actual = authDiskSource.getV2EncryptedMigrationsGracePeriodStart(userId = mockUserId)

assertEquals(expectedState, actual)
}

@Test
fun `getV2EncryptedMigrationsGracePeriodStart should pull null when there is no data`() {
val mockUserId = "mockUserId"

val actual = authDiskSource.getV2EncryptedMigrationsGracePeriodStart(userId = mockUserId)

assertNull(actual)
}

@Test
fun `storeV2EncryptedMigrationsGracePeriodStart should update SharedPreferences`() {
val mockUserId = "mockUserId"
val expectedState = Instant.parse("2025-01-13T12:00:00Z")

authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = mockUserId,
gracePeriodStart = expectedState,
)

assertEquals(
expectedState,
authDiskSource.getV2EncryptedMigrationsGracePeriodStart(userId = mockUserId),
)
}

@Test
fun `storeV2EncryptedMigrationsGracePeriodStart should clear the value when null is passed`() {
val mockUserId = "mockUserId"
authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = mockUserId,
gracePeriodStart = Instant.parse("2025-01-13T12:00:00Z"),
)

authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = mockUserId,
gracePeriodStart = null,
)

assertNull(authDiskSource.getV2EncryptedMigrationsGracePeriodStart(userId = mockUserId))
}
}

private const val USER_STATE_JSON = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class FakeAuthDiskSource : AuthDiskSource {
mutableMapOf<String, MutableSharedFlow<String?>>()
private val storedV2UpgradeTokens = mutableMapOf<String, V2UpgradeTokenJson?>()
private val storedUserKeyIds = mutableMapOf<String, String?>()
private val storedV2EncryptedMigrationsGracePeriodStarts = mutableMapOf<String, Instant?>()

override var userState: UserStateJson? = null
set(value) {
Expand Down Expand Up @@ -109,6 +110,12 @@ class FakeAuthDiskSource : AuthDiskSource {
mutableAccountTokensFlowMap.remove(userId)
mutableEphemeralPinProtectedUserKeyEnvelopesFlowMap.remove(userId)
mutablePersistentPinProtectedUserKeyEnvelopesFlowMap.remove(userId)

// Certain values are never removed as required by the feature requirements:
// * DeviceKey
// * PendingAuthRequest
// * OnboardingStatus
// * V2EncryptedMigrationsGracePeriodStart
}

override fun getShouldUseKeyConnectorFlow(
Expand Down Expand Up @@ -178,6 +185,16 @@ class FakeAuthDiskSource : AuthDiskSource {
storedV2UpgradeTokens[userId] = v2UpgradeToken
}

override fun getV2EncryptedMigrationsGracePeriodStart(userId: String): Instant? =
storedV2EncryptedMigrationsGracePeriodStarts[userId]

override fun storeV2EncryptedMigrationsGracePeriodStart(
userId: String,
gracePeriodStart: Instant?,
) {
storedV2EncryptedMigrationsGracePeriodStarts[userId] = gracePeriodStart
}

override fun getTwoFactorToken(email: String): String? = storedTwoFactorTokens[email]

override fun storeTwoFactorToken(email: String, twoFactorToken: String?) {
Expand Down Expand Up @@ -471,6 +488,13 @@ class FakeAuthDiskSource : AuthDiskSource {
assertEquals(v2UpgradeToken, storedV2UpgradeTokens[userId])
}

/**
* Assert that the [gracePeriodStart] was stored successfully using the [userId].
*/
fun assertV2EncryptedMigrationsGracePeriodStart(userId: String, gracePeriodStart: Instant?) {
assertEquals(gracePeriodStart, storedV2EncryptedMigrationsGracePeriodStarts[userId])
}

/**
* Assert that the [twoFactorToken] was stored successfully using the [email].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,48 @@ class SdkStateBridgeTest {
authDiskSource.assertV2UpgradeToken(userId = USER_ID, v2UpgradeToken = null)
}

@Test
fun `setV2EncryptedMigrationsGracePeriodStart should store the grace period start`() = runTest {
stateBridge.setV2EncryptedMigrationsGracePeriodStart(value = GRACE_PERIOD_START)

authDiskSource.assertV2EncryptedMigrationsGracePeriodStart(
userId = USER_ID,
gracePeriodStart = GRACE_PERIOD_START,
)
}

@Test
fun `getV2EncryptedMigrationsGracePeriodStart should return the stored grace period start`() =
runTest {
assertNull(stateBridge.getV2EncryptedMigrationsGracePeriodStart())

authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = USER_ID,
gracePeriodStart = GRACE_PERIOD_START,
)

assertEquals(
GRACE_PERIOD_START,
stateBridge.getV2EncryptedMigrationsGracePeriodStart(),
)
}

@Test
fun `clearV2EncryptedMigrationsGracePeriodStart should clear the grace period start`() =
runTest {
authDiskSource.storeV2EncryptedMigrationsGracePeriodStart(
userId = USER_ID,
gracePeriodStart = GRACE_PERIOD_START,
)

stateBridge.clearV2EncryptedMigrationsGracePeriodStart()

authDiskSource.assertV2EncryptedMigrationsGracePeriodStart(
userId = USER_ID,
gracePeriodStart = null,
)
}

@Test
fun `setAccountCryptographicState should store the account cryptographic state`() = runTest {
val state = createMockWrappedAccountCryptographicState(number = 1)
Expand Down Expand Up @@ -493,6 +535,8 @@ private val V2_UPGRADE_TOKEN_JSON: V2UpgradeTokenJson = V2UpgradeTokenJson(
wrappedUserKey2 = "wrappedUserKey2",
)

private val GRACE_PERIOD_START: Instant = Instant.parse("2024-09-13T01:00:00.00Z")

private val MASTER_PASSWORD_UNLOCK_DATA: MasterPasswordUnlockData = MasterPasswordUnlockData(
kdf = Kdf.Pbkdf2(iterations = 600_000u),
masterKeyWrappedUserKey = "masterKeyWrappedUserKey",
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ androidxRoom = "2.8.4"
androidxSecurityCrypto = "1.1.0"
androidxSplash = "1.2.0"
androidxWork = "2.11.2"
bitwardenSdk = "3.0.0-8495-c1b859a9"
bitwardenSdk = "3.0.0-8671-5d8ae614"
crashlytics = "3.0.8"
detekt = "1.23.8"
firebaseBom = "34.18.0"
Expand Down
Loading