Skip to content

Commit 535deae

Browse files
Update logic for handling the pin protected user key
1 parent 21afa81 commit 535deae

File tree

14 files changed

+463
-385
lines changed

14 files changed

+463
-385
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,6 @@ class AuthDiskSourceImpl(
145145
storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null)
146146
storeUserKey(userId = userId, userKey = null)
147147
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
148-
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
149-
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
150-
storeEncryptedPin(userId = userId, encryptedPin = null)
151148
storePrivateKey(userId = userId, privateKey = null)
152149
storeAccountKeys(userId = userId, accountKeys = null)
153150
storeOrganizationKeys(userId = userId, organizationKeys = null)
@@ -163,9 +160,13 @@ class AuthDiskSourceImpl(
163160
storeShowImportLogins(userId = userId, showImportLogins = null)
164161
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
165162

166-
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
167-
// indefinitely unless the TDE flow explicitly removes them.
168-
// Do not remove OnboardingStatus we want to keep track of this even after logout.
163+
// Certain values are never removed as required by the feature requirements:
164+
// * EncryptedPin
165+
// * PinProtectedUserKey
166+
// * PinProtectedUserKeyEnvelope
167+
// * DeviceKey
168+
// * PendingAuthRequest
169+
// * OnboardingStatus
169170
}
170171

171172
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,10 @@ class UserLogoutManagerImpl(
7777
if (isExpired) {
7878
showToast(message = BitwardenString.login_expired)
7979
}
80-
authDiskSource.storeAccountTokens(
81-
userId = userId,
82-
accountTokens = null,
83-
)
8480

8581
// Save any data that will still need to be retained after otherwise clearing all dat
8682
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
8783
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
88-
val pinProtectedUserKeyEnvelope = authDiskSource
89-
.getPinProtectedUserKeyEnvelope(userId = userId)
9084

9185
switchUserIfAvailable(
9286
currentUserId = userId,
@@ -108,10 +102,6 @@ class UserLogoutManagerImpl(
108102
vaultTimeoutAction = vaultTimeoutAction,
109103
)
110104
}
111-
authDiskSource.storePinProtectedUserKeyEnvelope(
112-
userId = userId,
113-
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
114-
)
115105
}
116106

117107
private fun clearData(userId: String) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.x8bit.bitwarden.data.vault.manager
2+
3+
/**
4+
* Manager class to manage the pin-protected user key.
5+
*/
6+
interface PinProtectedUserKeyManager {
7+
/**
8+
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected
9+
* user key. This indicates a scenario in which a user has requested PIN unlocking but requires
10+
* master-password unlocking on app restart. This function may then be called after such an
11+
* unlock to derive a pin-protected user key and store it in memory for use for any subsequent
12+
* unlocks during this current app session.
13+
*
14+
* If the user's vault has not yet been unlocked, this call will do nothing.
15+
*
16+
* @param userId The ID of the user to check.
17+
*/
18+
suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String)
19+
20+
/**
21+
* Migrates the PIN-protected user key for the given user if needed.
22+
*
23+
* If an encrypted PIN exists and no PIN-protected user key envelope is present, enrolls the
24+
* PIN with the encrypted PIN and stores the resulting envelope.
25+
* Optionally marks the envelope as in-memory only if the PIN-protected user key is not present.
26+
*
27+
* @param userId The ID of the user for whom to migrate the PIN-protected user key.
28+
*/
29+
suspend fun migratePinProtectedUserKeyIfNeeded(userId: String)
30+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.x8bit.bitwarden.data.vault.manager
2+
3+
import com.bitwarden.core.EnrollPinResponse
4+
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
5+
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
6+
import timber.log.Timber
7+
8+
/**
9+
* The default implementation of the [PinProtectedUserKeyManager].
10+
*/
11+
internal class PinProtectedUserKeyManagerImpl(
12+
private val authDiskSource: AuthDiskSource,
13+
private val vaultSdkSource: VaultSdkSource,
14+
) : PinProtectedUserKeyManager {
15+
override suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String) {
16+
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
17+
if (authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) != null) return
18+
19+
this
20+
.enrollPinWithEncryptedPin(
21+
userId = userId,
22+
encryptedPin = encryptedPin,
23+
inMemoryOnly = true,
24+
)
25+
.onSuccess {
26+
Timber.d("[Auth] Set PIN-protected user key in memory")
27+
}
28+
}
29+
30+
override suspend fun migratePinProtectedUserKeyIfNeeded(userId: String) {
31+
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
32+
if (authDiskSource.getPinProtectedUserKeyEnvelope(userId = userId) != null) return
33+
34+
val inMemoryOnly = authDiskSource.getPinProtectedUserKey(userId = userId) == null
35+
this
36+
.enrollPinWithEncryptedPin(
37+
userId = userId,
38+
encryptedPin = encryptedPin,
39+
inMemoryOnly = inMemoryOnly,
40+
)
41+
.onSuccess {
42+
if (inMemoryOnly) {
43+
Timber.d("[Auth] Set PIN-protected user key in memory")
44+
} else {
45+
Timber.d("[Auth] Migrated from legacy PIN to PIN-protected user key envelope")
46+
}
47+
}
48+
}
49+
50+
private suspend fun enrollPinWithEncryptedPin(
51+
userId: String,
52+
encryptedPin: String,
53+
inMemoryOnly: Boolean,
54+
): Result<EnrollPinResponse> =
55+
vaultSdkSource
56+
.enrollPinWithEncryptedPin(userId = userId, encryptedPin = encryptedPin)
57+
.onSuccess { enrollPinResponse ->
58+
storePinData(
59+
userId = userId,
60+
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
61+
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
62+
inMemoryOnly = inMemoryOnly,
63+
)
64+
}
65+
.onFailure {
66+
storePinData(
67+
userId = userId,
68+
encryptedPin = null,
69+
pinProtectedUserKeyEnvelope = null,
70+
inMemoryOnly = false,
71+
)
72+
}
73+
74+
private fun storePinData(
75+
userId: String,
76+
encryptedPin: String?,
77+
pinProtectedUserKeyEnvelope: String?,
78+
inMemoryOnly: Boolean,
79+
) {
80+
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
81+
authDiskSource.storePinProtectedUserKeyEnvelope(
82+
userId = userId,
83+
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
84+
inMemoryOnly = inMemoryOnly,
85+
)
86+
// This property is deprecated and we should be migrated to the PinProtectedUserKeyEnvelope.
87+
// Because of this, we always clear this value and it should always be cleared at the disk
88+
// level, not the in-memory level.
89+
authDiskSource.storePinProtectedUserKey(
90+
userId = userId,
91+
pinProtectedUserKey = null,
92+
inMemoryOnly = false,
93+
)
94+
}
95+
}

app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class VaultLockManagerImpl(
9191
private val userLogoutManager: UserLogoutManager,
9292
private val trustedDeviceManager: TrustedDeviceManager,
9393
private val kdfManager: KdfManager,
94+
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager,
9495
dispatcherManager: DispatcherManager,
9596
context: Context,
9697
) : VaultLockManager {
@@ -234,7 +235,8 @@ class VaultLockManagerImpl(
234235
trustedDeviceManager
235236
.trustThisDeviceIfNecessary(userId = userId)
236237
updateKdfIfNeeded(initUserCryptoMethod)
237-
migratePinProtectedUserKeyIfNeeded(userId = userId)
238+
pinProtectedUserKeyManager
239+
.migratePinProtectedUserKeyIfNeeded(userId = userId)
238240
setVaultToUnlocked(userId = userId)
239241
} else {
240242
incrementInvalidUnlockCount(userId = userId)
@@ -308,47 +310,6 @@ class VaultLockManagerImpl(
308310
)
309311
}
310312

311-
/**
312-
* Migrates the PIN-protected user key for the given user if needed.
313-
*
314-
* If an encrypted PIN exists and no PIN-protected user key envelope is present,
315-
* enrolls the PIN with the encrypted PIN and stores the resulting envelope.
316-
* Optionally marks the envelope as in-memory only if the PIN-protected user key is not present.
317-
*
318-
* @param userId The ID of the user for whom to migrate the PIN-protected user key.
319-
*/
320-
private suspend fun migratePinProtectedUserKeyIfNeeded(
321-
userId: String,
322-
) {
323-
val encryptedPin = authDiskSource.getEncryptedPin(userId) ?: return
324-
if (authDiskSource.getPinProtectedUserKeyEnvelope(userId) != null) return
325-
326-
val inMemoryOnly = authDiskSource.getPinProtectedUserKey(userId) == null
327-
328-
vaultSdkSource.enrollPinWithEncryptedPin(userId, encryptedPin)
329-
.onSuccess { enrollPinResponse ->
330-
authDiskSource.storeEncryptedPin(
331-
userId = userId,
332-
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
333-
)
334-
authDiskSource.storePinProtectedUserKeyEnvelope(
335-
userId = userId,
336-
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
337-
inMemoryOnly = inMemoryOnly,
338-
)
339-
authDiskSource.storePinProtectedUserKey(
340-
userId = userId,
341-
pinProtectedUserKey = null,
342-
inMemoryOnly = inMemoryOnly,
343-
)
344-
if (inMemoryOnly) {
345-
Timber.d("[Auth] Set PIN-protected user key in memory")
346-
} else {
347-
Timber.d("[Auth] Migrated from legacy PIN to PIN-protected user key envelope")
348-
}
349-
}
350-
}
351-
352313
/**
353314
* Increments the stored invalid unlock count for the given [userId] and automatically logs out
354315
* if this new value is greater than [MAXIMUM_INVALID_UNLOCK_ATTEMPTS].

app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import com.x8bit.bitwarden.data.vault.manager.FileManager
3131
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
3232
import com.x8bit.bitwarden.data.vault.manager.FolderManager
3333
import com.x8bit.bitwarden.data.vault.manager.FolderManagerImpl
34+
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager
35+
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManagerImpl
3436
import com.x8bit.bitwarden.data.vault.manager.SendManager
3537
import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl
3638
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
@@ -146,6 +148,7 @@ object VaultManagerModule {
146148
dispatcherManager: DispatcherManager,
147149
trustedDeviceManager: TrustedDeviceManager,
148150
kdfManager: KdfManager,
151+
pinProtectedUserKeyManager: PinProtectedUserKeyManager,
149152
): VaultLockManager =
150153
VaultLockManagerImpl(
151154
context = context,
@@ -160,6 +163,18 @@ object VaultManagerModule {
160163
dispatcherManager = dispatcherManager,
161164
trustedDeviceManager = trustedDeviceManager,
162165
kdfManager = kdfManager,
166+
pinProtectedUserKeyManager = pinProtectedUserKeyManager,
167+
)
168+
169+
@Provides
170+
@Singleton
171+
fun providePinProtectedUserKeyManager(
172+
authDiskSource: AuthDiskSource,
173+
vaultSdkSource: VaultSdkSource,
174+
): PinProtectedUserKeyManager =
175+
PinProtectedUserKeyManagerImpl(
176+
authDiskSource = authDiskSource,
177+
vaultSdkSource = vaultSdkSource,
163178
)
164179

165180
@Provides

app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
2828
import com.x8bit.bitwarden.data.vault.manager.CipherManager
2929
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
3030
import com.x8bit.bitwarden.data.vault.manager.FolderManager
31+
import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager
3132
import com.x8bit.bitwarden.data.vault.manager.SendManager
3233
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
3334
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
@@ -40,7 +41,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
4041
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
4142
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
4243
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
43-
import com.x8bit.bitwarden.data.vault.repository.util.logTag
4444
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
4545
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
4646
import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount
@@ -79,6 +79,7 @@ class VaultRepositoryImpl(
7979
private val totpCodeManager: TotpCodeManager,
8080
private val vaultSyncManager: VaultSyncManager,
8181
private val credentialExchangeImportManager: CredentialExchangeImportManager,
82+
private val pinProtectedUserKeyManager: PinProtectedUserKeyManager,
8283
dispatcherManager: DispatcherManager,
8384
) : VaultRepository,
8485
CipherManager by cipherManager,
@@ -328,11 +329,8 @@ class VaultRepositoryImpl(
328329
)
329330
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
330331
}
331-
deriveTemporaryPinProtectedUserKeyIfNecessary(
332+
pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
332333
userId = userId,
333-
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
334-
decryptedUserKey = decryptedUserKey,
335-
),
336334
)
337335
}
338336
}
@@ -369,9 +367,8 @@ class VaultRepositoryImpl(
369367
)
370368
.also {
371369
if (it is VaultUnlockResult.Success) {
372-
deriveTemporaryPinProtectedUserKeyIfNecessary(
370+
pinProtectedUserKeyManager.deriveTemporaryPinProtectedUserKeyIfNecessary(
373371
userId = userId,
374-
initUserCryptoMethod = initUserCryptoMethod,
375372
)
376373
}
377374
}
@@ -530,54 +527,6 @@ class VaultRepositoryImpl(
530527
)
531528
}
532529

533-
/**
534-
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
535-
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
536-
* master-password unlocking on app restart. This function may then be called after such an
537-
* unlock to derive a pin-protected user key and store it in memory for use for any subsequent
538-
* unlocks during this current app session.
539-
*
540-
* If the user's vault has not yet been unlocked, this call will do nothing.
541-
*
542-
* @param userId The ID of the user to check.
543-
* @param initUserCryptoMethod The method used to initialize the user's crypto.
544-
*/
545-
private suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(
546-
userId: String,
547-
initUserCryptoMethod: InitUserCryptoMethod,
548-
) {
549-
Timber.d("[Auth] Vault unlocked, method: ${initUserCryptoMethod.logTag}")
550-
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
551-
val existingPinProtectedUserKeyEnvelope = authDiskSource
552-
.getPinProtectedUserKeyEnvelope(
553-
userId = userId,
554-
)
555-
if (existingPinProtectedUserKeyEnvelope != null) return
556-
557-
vaultSdkSource
558-
.enrollPinWithEncryptedPin(
559-
userId = userId,
560-
encryptedPin = encryptedPin,
561-
)
562-
.onSuccess { enrollPinResponse ->
563-
authDiskSource.storeEncryptedPin(
564-
userId = userId,
565-
encryptedPin = enrollPinResponse.userKeyEncryptedPin,
566-
)
567-
authDiskSource.storePinProtectedUserKeyEnvelope(
568-
userId = userId,
569-
pinProtectedUserKeyEnvelope = enrollPinResponse.pinProtectedUserKeyEnvelope,
570-
inMemoryOnly = true,
571-
)
572-
authDiskSource.storePinProtectedUserKey(
573-
userId = userId,
574-
pinProtectedUserKey = null,
575-
inMemoryOnly = true,
576-
)
577-
Timber.d("[Auth] Set PIN-protected user key in memory")
578-
}
579-
}
580-
581530
private suspend fun unlockVaultForUser(
582531
userId: String,
583532
initUserCryptoMethod: InitUserCryptoMethod,

0 commit comments

Comments
 (0)