Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -17,7 +17,7 @@ extension BitwardenSdk.InitUserCryptoMethod {
case .masterPasswordUnlock:
"Master Password Unlock"
case .password:
"Password"
"Password (Legacy - Deprecated)"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ’ญ Deprecation strategy

Good to mark this as "Legacy - Deprecated". Since this PR removes .password usage from the vault unlock flow but the enum case remains in the SDK, consider:

  1. Adding a code comment documenting when/if this case can be fully removed
  2. If it must remain for SDK compatibility, document which scenarios still require it
  3. If removal is planned, consider adding a @available deprecation attribute when Swift's SDK supports it

This helps future maintainers understand the deprecation timeline.

case .pin:
"PIN"
case .pinEnvelope:
Expand Down
22 changes: 10 additions & 12 deletions BitwardenShared/Core/Auth/Repositories/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -993,17 +993,15 @@ extension DefaultAuthRepository: AuthRepository {
func unlockVaultWithPassword(password: String) async throws {
let account = try await stateService.getActiveAccount()
let encryptionKeys = try await stateService.getAccountEncryptionKeys(userId: account.profile.userId)
guard let encUserKey = encryptionKeys.encryptedUserKey else { throw StateServiceError.noEncUserKey }

let masterPasswordUnlock = account.profile.userDecryptionOptions?.masterPasswordUnlock
let unlockMethod: InitUserCryptoMethod = if let masterPasswordUnlock {
.masterPasswordUnlock(
password: password,
masterPasswordUnlock: MasterPasswordUnlockData(responseModel: masterPasswordUnlock),
)
} else {
.password(password: password, userKey: encUserKey)
guard let masterPasswordUnlock = account.profile.userDecryptionOptions?.masterPasswordUnlock else {
throw AuthError.missingMasterPasswordUnlockData
}

let unlockMethod: InitUserCryptoMethod = .masterPasswordUnlock(
password: password,
masterPasswordUnlock: MasterPasswordUnlockData(responseModel: masterPasswordUnlock),
)
try await unlockVault(method: unlockMethod)

let hashedPassword = try await authService.hashPassword(
Expand Down Expand Up @@ -1249,8 +1247,7 @@ extension DefaultAuthRepository: AuthRepository {
)
await flightRecorder.log("[Auth] Migrated from legacy PIN to PIN-protected user key envelope")
case .decryptedKey,
.masterPasswordUnlock,
.password:
.masterPasswordUnlock:
guard let encryptedPin = try await stateService.getEncryptedPin(),
try await stateService.pinProtectedUserKeyEnvelope() == nil
else {
Expand All @@ -1276,7 +1273,8 @@ extension DefaultAuthRepository: AuthRepository {
try await stateService.setPinProtectedUserKeyToMemory(enrollPinResponse.pinProtectedUserKeyEnvelope)
await flightRecorder.log("[Auth] Set PIN-protected user key in memory")
}
case .pinEnvelope:
case .password,
.pinEnvelope:
break
}
}
Expand Down
166 changes: 147 additions & 19 deletions BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,18 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
/// `setMasterPassword()` sets the user's master password, saves their encryption keys and
/// unlocks the vault.
func test_setMasterPassword() async throws {
let account = Account.fixture()
let account = Account.fixture(profile: .fixture(
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "encryptedUserKey",
salt: "SALT",
),
keyConnectorOption: nil,
trustedDeviceOption: nil,
),
))
client.result = .httpSuccess(testData: .emptyResponse)
// Account encryption keys don't exist until after a MP has been set for non-TDE users.
stateService.accountEncryptionKeys["1"] = nil
Expand Down Expand Up @@ -1216,7 +1227,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "private",
signingKey: nil,
securityState: nil,
method: .password(password: "NEW_PASSWORD", userKey: "encryptedUserKey"),
method: .masterPasswordUnlock(
password: "NEW_PASSWORD",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
masterKeyWrappedUserKey: "encryptedUserKey",
salt: "SALT",
),
),
),
)
}
Expand Down Expand Up @@ -1258,7 +1276,13 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
encryptedPrivateKey: "PRIVATE_KEY",
encryptedUserKey: "KEY",
)
stateService.activeAccount = Account.fixtureWithTdeNoPassword()
var tdeAccount = Account.fixtureWithTdeNoPassword()
tdeAccount.profile.userDecryptionOptions?.masterPasswordUnlock = MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "NEW_KEY",
salt: "SALT",
)
stateService.activeAccount = tdeAccount

try await subject.setMasterPassword(
"NEW_PASSWORD",
Expand Down Expand Up @@ -1301,7 +1325,12 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
/// `setMasterPassword()` sets the user's master password, saves their encryption keys and
/// unlocks the vault.
func test_setMasterPassword_TDE() async throws {
let account = Account.fixtureWithTDE()
var account = Account.fixtureWithTDE()
account.profile.userDecryptionOptions?.masterPasswordUnlock = MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "NEW_KEY",
salt: "SALT",
)
client.result = .httpSuccess(testData: .emptyResponse)
clientService.mockCrypto.makeUpdatePasswordResult = .success(
UpdatePasswordResponse(passwordHash: "NEW_PASSWORD_HASH", newKey: "NEW_KEY"),
Expand Down Expand Up @@ -1347,7 +1376,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "PRIVATE_KEY",
signingKey: nil,
securityState: nil,
method: .password(password: "NEW_PASSWORD", userKey: "NEW_KEY"),
method: .masterPasswordUnlock(
password: "NEW_PASSWORD",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
masterKeyWrappedUserKey: "NEW_KEY",
salt: "SALT",
),
),
),
)
}
Expand Down Expand Up @@ -1797,7 +1833,18 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo

/// `unlockVaultWithPassword(password:)` unlocks the vault with the user's password.
func test_unlockVault() async throws {
stateService.activeAccount = .fixture()
stateService.activeAccount = .fixture(profile: .fixture(
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
keyConnectorOption: nil,
trustedDeviceOption: nil,
),
))
stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(
accountKeys: .fixtureFilled(),
Expand All @@ -1820,7 +1867,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "PRIVATE_KEY",
signingKey: "WRAPPED_SIGNING_KEY",
securityState: "SECURITY_STATE",
method: .password(password: "password", userKey: "USER_KEY"),
method: .masterPasswordUnlock(
password: "password",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
masterKeyWrappedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
),
),
)
XCTAssertFalse(vaultTimeoutService.isLocked(userId: "1"))
Expand Down Expand Up @@ -2086,7 +2140,19 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
// `unlockVaultWithPassword(_:)` unlocks the vault with the user's password and checks if the
// user's KDF settings need to be updated.
func test_unlockVaultWithPassword_checksForKdfUpdate() async throws {
let account = Account.fixture(profile: .fixture(kdfIterations: 100_000))
let account = Account.fixture(profile: .fixture(
kdfIterations: 100_000,
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: 100_000),
masterKeyEncryptedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
keyConnectorOption: nil,
trustedDeviceOption: nil,
),
))
configService.featureFlagsBool[.forceUpdateKdfSettings] = false
changeKdfService.needsKdfUpdateToMinimumsResult = true
stateService.activeAccount = account
Expand All @@ -2109,7 +2175,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "PRIVATE_KEY",
signingKey: "WRAPPED_SIGNING_KEY",
securityState: "SECURITY_STATE",
method: .password(password: "password", userKey: "USER_KEY"),
method: .masterPasswordUnlock(
password: "password",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: 100_000),
masterKeyWrappedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
),
),
)
XCTAssertFalse(vaultTimeoutService.isLocked(userId: "1"))
Expand All @@ -2125,7 +2198,19 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
// user's KDF settings need to be updated. If updating the user's KDF fails, an error is logged
// but vault unlock still succeeds.
func test_unlockVaultWithPassword_checksForKdfUpdate_error() async throws {
let account = Account.fixture(profile: .fixture(kdfIterations: 100_000))
let account = Account.fixture(profile: .fixture(
kdfIterations: 100_000,
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: 100_000),
masterKeyEncryptedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
keyConnectorOption: nil,
trustedDeviceOption: nil,
),
))
configService.featureFlagsBool[.forceUpdateKdfSettings] = false
changeKdfService.needsKdfUpdateToMinimumsResult = true
changeKdfService.updateKdfToMinimumsResult = .failure(BitwardenTestError.example)
Expand All @@ -2151,7 +2236,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "PRIVATE_KEY",
signingKey: "WRAPPED_SIGNING_KEY",
securityState: "SECURITY_STATE",
method: .password(password: "password", userKey: "USER_KEY"),
method: .masterPasswordUnlock(
password: "password",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: 100_000),
masterKeyWrappedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
),
),
)
XCTAssertFalse(vaultTimeoutService.isLocked(userId: "1"))
Expand All @@ -2170,7 +2262,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: 600_000),
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
Expand Down Expand Up @@ -2203,7 +2295,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
method: .masterPasswordUnlock(
password: "password",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: 600_000),
kdf: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
masterKeyWrappedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
Expand Down Expand Up @@ -2405,7 +2497,18 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
// `unlockVaultWithPassword(_:)` unlocks the vault with the user's password and migrates the
// legacy pin keys.
func test_unlockVaultWithPassword_migratesPinProtectedUserKey() async throws {
let account = Account.fixture()
let account = Account.fixture(profile: .fixture(
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
keyConnectorOption: nil,
trustedDeviceOption: nil,
),
))
clientService.mockCrypto.enrollPinWithEncryptedPinResult = .success(
EnrollPinResponse(
pinProtectedUserKeyEnvelope: "pinProtectedUserKeyEnvelope",
Expand Down Expand Up @@ -2434,7 +2537,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "PRIVATE_KEY",
signingKey: "WRAPPED_SIGNING_KEY",
securityState: "SECURITY_STATE",
method: .password(password: "password", userKey: "USER_KEY"),
method: .masterPasswordUnlock(
password: "password",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
masterKeyWrappedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
),
),
)
XCTAssertFalse(vaultTimeoutService.isLocked(userId: "1"))
Expand All @@ -2447,15 +2557,26 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
XCTAssertEqual(stateService.encryptedPinByUserId["1"], "userKeyEncryptedPin")
XCTAssertEqual(stateService.pinProtectedUserKeyEnvelopeValue["1"], "pinProtectedUserKeyEnvelope")
XCTAssertEqual(flightRecorder.logMessages, [
"[Auth] Vault unlocked, method: Password",
"[Auth] Vault unlocked, method: Master Password Unlock",
"[Auth] Migrated from legacy PIN to PIN-protected user key envelope",
])
}

// `unlockVaultWithPassword(_:)` unlocks the vault with the user's password and sets the
// PIN-protected user key in memory.
func test_unlockVaultWithPassword_setsPinProtectedUserKeyInMemory() async throws {
let account = Account.fixture()
let account = Account.fixture(profile: .fixture(
userDecryptionOptions: UserDecryptionOptions(
hasMasterPassword: true,
masterPasswordUnlock: MasterPasswordUnlockResponseModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations),
masterKeyEncryptedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
keyConnectorOption: nil,
trustedDeviceOption: nil,
),
))
clientService.mockCrypto.enrollPinWithEncryptedPinResult = .success(
EnrollPinResponse(
pinProtectedUserKeyEnvelope: "pinProtectedUserKeyEnvelope",
Expand Down Expand Up @@ -2483,7 +2604,14 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
privateKey: "PRIVATE_KEY",
signingKey: "WRAPPED_SIGNING_KEY",
securityState: "SECURITY_STATE",
method: .password(password: "password", userKey: "USER_KEY"),
method: .masterPasswordUnlock(
password: "password",
masterPasswordUnlock: MasterPasswordUnlockData(
kdf: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
masterKeyWrappedUserKey: "MASTER_KEY_ENCRYPTED_USER_KEY",
salt: "SALT",
),
),
),
)
XCTAssertFalse(vaultTimeoutService.isLocked(userId: "1"))
Expand All @@ -2495,7 +2623,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
XCTAssertEqual(stateService.encryptedPinByUserId["1"], "encryptedPin")
XCTAssertNil(stateService.pinProtectedUserKeyEnvelopeValue["1"])
XCTAssertEqual(flightRecorder.logMessages, [
"[Auth] Vault unlocked, method: Password",
"[Auth] Vault unlocked, method: Master Password Unlock",
"[Auth] Set PIN-protected user key in memory",
])
}
Expand Down
3 changes: 3 additions & 0 deletions BitwardenShared/Core/Auth/Services/AuthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ enum AuthError: Error {
/// The key used for login with device was missing.
case missingLoginWithDeviceKey

/// The data for the master password unlock method was missing.
case missingMasterPasswordUnlockData

/// The request that should have been cached for the two-factor authentication method was missing.
case missingTwoFactorRequest

Expand Down
Loading
Loading