Skip to content

Commit e81760c

Browse files
[PM-23277] Force KDF updates if below minimums
1 parent 7741d70 commit e81760c

File tree

15 files changed

+564
-0
lines changed

15 files changed

+564
-0
lines changed

BitwardenResources/Localizations/en.lproj/Localizable.strings

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,3 +1278,7 @@
12781278
"WhenUsingTwoStepVerification" = "When using 2-step verification, you’ll enter your username and password and a code generated in this app.";
12791279
"YesSetDefault" = "Yes, set default";
12801280
"YouCanUpdateYourDefaultAnytimeInSettings" = "You can update your default anytime in settings.";
1281+
"UpdateYourEncryptionSettings" = "Update your encryption settings";
1282+
"TheNewRecommendedEncryptionSettingsDescriptionLong" = "The new recommended encryption settings will improve your account security. Enter your master password to update now.";
1283+
"Updating" = "Updating...";
1284+
"EncryptionSettingsUpdated" = "Encryption settings updated";

BitwardenShared/Core/Auth/Repositories/AuthRepository.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,9 @@ class DefaultAuthRepository {
420420
/// The service to use system Biometrics for vault unlock.
421421
let biometricsRepository: BiometricsRepository
422422

423+
/// The service used to change the user's KDF settings.
424+
private let changeKdfService: ChangeKdfService
425+
423426
/// The service that handles common client functionality such as encryption and decryption.
424427
private let clientService: ClientService
425428

@@ -468,6 +471,7 @@ class DefaultAuthRepository {
468471
/// - appContextHelper: The helper to know about the app context.
469472
/// - authService: The service used that handles some of the auth logic.
470473
/// - biometricsRepository: The service to use system Biometrics for vault unlock.
474+
/// - changeKdfService: The service used to change the user's KDF settings.
471475
/// - clientService: The service that handles common client functionality such as encryption and decryption.
472476
/// - configService: The service to get server-specified configuration.
473477
/// - environmentService: The service used by the application to manage the environment settings.
@@ -488,6 +492,7 @@ class DefaultAuthRepository {
488492
appContextHelper: AppContextHelper,
489493
authService: AuthService,
490494
biometricsRepository: BiometricsRepository,
495+
changeKdfService: ChangeKdfService,
491496
clientService: ClientService,
492497
configService: ConfigService,
493498
environmentService: EnvironmentService,
@@ -506,6 +511,7 @@ class DefaultAuthRepository {
506511
self.appContextHelper = appContextHelper
507512
self.authService = authService
508513
self.biometricsRepository = biometricsRepository
514+
self.changeKdfService = changeKdfService
509515
self.clientService = clientService
510516
self.configService = configService
511517
self.environmentService = environmentService
@@ -1105,6 +1111,7 @@ extension DefaultAuthRepository: AuthRepository {
11051111
purpose: .localAuthorization
11061112
)
11071113
try await stateService.setMasterPasswordHash(hashedPassword)
1114+
try await changeKdfService.updateKdfToMinimumsIfNeeded(password: password)
11081115
case .decryptedKey,
11091116
.deviceKey,
11101117
.keyConnector,

BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
1515
var appContextHelper: MockAppContextHelper!
1616
var authService: MockAuthService!
1717
var biometricsRepository: MockBiometricsRepository!
18+
var changeKdfService: MockChangeKdfService!
1819
var client: MockHTTPClient!
1920
var clientService: MockClientService!
2021
var configService: MockConfigService!
@@ -96,6 +97,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
9697
accountAPIService = APIService(client: client)
9798
authService = MockAuthService()
9899
biometricsRepository = MockBiometricsRepository()
100+
changeKdfService = MockChangeKdfService()
99101
configService = MockConfigService()
100102
environmentService = MockEnvironmentService()
101103
errorReporter = MockErrorReporter()
@@ -112,6 +114,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
112114
appContextHelper: appContextHelper,
113115
authService: authService,
114116
biometricsRepository: biometricsRepository,
117+
changeKdfService: changeKdfService,
115118
clientService: clientService,
116119
configService: configService,
117120
environmentService: environmentService,
@@ -135,6 +138,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
135138
appContextHelper = nil
136139
authService = nil
137140
biometricsRepository = nil
141+
changeKdfService = nil
138142
client = nil
139143
clientService = nil
140144
configService = nil
@@ -1977,6 +1981,40 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
19771981
}
19781982
}
19791983

1984+
// `unlockVaultWithPassword(_:)` unlocks the vault with the user's password and checks if the
1985+
// user's KDF settings need to be updated.
1986+
func test_unlockVaultWithPassword_checksForKdfUpdate() async throws {
1987+
let account = Account.fixture(profile: .fixture(kdfIterations: 100_000))
1988+
configService.featureFlagsBool[.forceUpdateKdfSettings] = false
1989+
changeKdfService.needsKdfUpdateToMinimumsResult = true
1990+
stateService.activeAccount = account
1991+
stateService.accountEncryptionKeys = [
1992+
"1": AccountEncryptionKeys(encryptedPrivateKey: "PRIVATE_KEY", encryptedUserKey: "USER_KEY"),
1993+
]
1994+
1995+
try await subject.unlockVaultWithPassword(password: "password")
1996+
1997+
XCTAssertEqual(
1998+
clientService.mockCrypto.initializeUserCryptoRequest,
1999+
InitUserCryptoRequest(
2000+
userId: "1",
2001+
kdfParams: .pbkdf2(iterations: UInt32(100_000)),
2002+
email: "user@bitwarden.com",
2003+
privateKey: "PRIVATE_KEY",
2004+
signingKey: nil,
2005+
securityState: nil,
2006+
method: .password(password: "password", userKey: "USER_KEY")
2007+
)
2008+
)
2009+
XCTAssertFalse(vaultTimeoutService.isLocked(userId: "1"))
2010+
XCTAssertTrue(vaultTimeoutService.unlockVaultHadUserInteraction)
2011+
XCTAssertEqual(stateService.manuallyLockedAccounts["1"], false)
2012+
2013+
XCTAssertTrue(changeKdfService.needsKdfUpdateToMinimumsCalled)
2014+
XCTAssertTrue(changeKdfService.updateKdfToMinimumsCalled)
2015+
XCTAssertEqual(changeKdfService.updateKdfToMinimumsPassword, "password")
2016+
}
2017+
19802018
/// `logout` throws an error with no accounts.
19812019
func test_logout_noAccounts() async {
19822020
stateService.accounts = []
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import BitwardenKit
2+
3+
// MARK: - ChangeKdfService
4+
5+
/// A protocol for a `ChangeKdfService` which handles updating the KDF settings for a user.
6+
///
7+
protocol ChangeKdfService {
8+
/// Returns whether the user needs to update their KDF settings to the minimum.
9+
///
10+
/// - Returns: Whether the user needs to update their KDF settings to the minimum.
11+
///
12+
func needsKdfUpdateToMinimums() async -> Bool
13+
14+
/// Updates the user's KDF settings to the minimums.
15+
///
16+
/// - Parameter password: The user's master password.
17+
///
18+
func updateKdfToMinimums(password: String) async throws
19+
}
20+
21+
extension ChangeKdfService {
22+
/// Updates the user's KDF settings to the minimums if their current settings are below the minimums.
23+
///
24+
/// - Parameter password: The user's master password.
25+
///
26+
func updateKdfToMinimumsIfNeeded(password: String) async throws {
27+
guard await needsKdfUpdateToMinimums() else { return }
28+
try await updateKdfToMinimums(password: password)
29+
}
30+
}
31+
32+
// MARK: - DefaultChangeKdfService
33+
34+
/// A default implementation of a `ChangeKdfService`.
35+
///
36+
class DefaultChangeKdfService: ChangeKdfService {
37+
// MARK: Properties
38+
39+
/// The services used by the application to make account related API requests.
40+
private let accountAPIService: AccountAPIService
41+
42+
/// The service that handles common client functionality such as encryption and decryption.
43+
private let clientService: ClientService
44+
45+
/// The service to get server-specified configuration.
46+
private let configService: ConfigService
47+
48+
/// The service used by the application to report non-fatal errors.
49+
private let errorReporter: ErrorReporter
50+
51+
/// The service used by the application to manage account state.
52+
private let stateService: StateService
53+
54+
// MARK: Initialization
55+
56+
/// Initialize a `DefaultChangeKdfService`.
57+
///
58+
/// - Parameters:
59+
/// - accountAPIService: The services used by the application to make account related API requests.
60+
/// - clientService: The service that handles common client functionality such as encryption and decryption.
61+
/// - configService: The service to get server-specified configuration.
62+
/// - errorReporter: The service used by the application to report non-fatal errors.
63+
/// - stateService: The service used by the application to manage account state.
64+
///
65+
init(
66+
accountAPIService: AccountAPIService,
67+
clientService: ClientService,
68+
configService: ConfigService,
69+
errorReporter: ErrorReporter,
70+
stateService: StateService
71+
) {
72+
self.accountAPIService = accountAPIService
73+
self.clientService = clientService
74+
self.configService = configService
75+
self.errorReporter = errorReporter
76+
self.stateService = stateService
77+
}
78+
79+
// MARK: Methods
80+
81+
func needsKdfUpdateToMinimums() async -> Bool {
82+
guard await configService.getFeatureFlag(.forceUpdateKdfSettings) else { return false }
83+
84+
do {
85+
let account = try await stateService.getActiveAccount()
86+
guard account.kdf.kdfType == .pbkdf2sha256,
87+
account.kdf.kdfIterations < Constants.minimumPbkdf2IterationsForUpgrade else {
88+
return false
89+
}
90+
return true
91+
} catch {
92+
errorReporter.log(error: error)
93+
return false
94+
}
95+
}
96+
97+
func updateKdfToMinimums(password: String) async throws {
98+
guard await configService.getFeatureFlag(.forceUpdateKdfSettings) else { return }
99+
100+
let account = try await stateService.getActiveAccount()
101+
102+
do {
103+
let updateKdfResponse = try await clientService.crypto().makeUpdateKdf(
104+
password: password,
105+
kdf: account.kdf.sdkKdf
106+
)
107+
try await accountAPIService.updateKdf(
108+
UpdateKdfRequestModel(response: updateKdfResponse)
109+
)
110+
111+
// TODO: Do we need to save updated KDF settings to Account/UserDefaults?
112+
113+
} catch {
114+
// If an error occurs, log the error. Don't throw since that would block the vault unlocking.
115+
errorReporter.log(error: BitwardenError.generalError(
116+
type: "Force Update KDF Error",
117+
message: "Unable to update KDF settings (\(account.kdf)",
118+
error: error
119+
))
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)