Skip to content

Commit b3a2485

Browse files
[PM-23277] Force KDF updates if below minimums (#1880)
1 parent db7cca7 commit b3a2485

33 files changed

+1164
-5
lines changed

BitwardenResources/Localizations/en.lproj/Localizable.strings

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,3 +1287,7 @@
12871287
"WarningStartsWithIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "**Warning:** “Starts with” is an advanced option with increased risk of exposing credentials.";
12881288
"WarningRegularExpressionIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "**Warning:** “Regular expression” is an advanced option with increased risk of exposing credentials if used incorrectly.";
12891289
"DefaultX" = "Default (%1$@)";
1290+
"UpdateYourEncryptionSettings" = "Update your encryption settings";
1291+
"TheNewRecommendedEncryptionSettingsDescriptionLong" = "The new recommended encryption settings will improve your account security. Enter your master password to update now.";
1292+
"Updating" = "Updating...";
1293+
"EncryptionSettingsUpdated" = "Encryption settings updated";

BitwardenShared/Core/Auth/Models/Domains/KdfConfig.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import BitwardenKit
2+
import BitwardenSdk
23

34
// MARK: - KdfConfig
45

56
/// A model for configuring KDF options.
67
///
78
struct KdfConfig: Codable, Equatable, Hashable {
9+
// MARK: Type Properties
10+
11+
/// The default `KdfConfig` used for new accounts or when upgrading the KDF config to minimums.
12+
static let defaultKdfConfig = KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations)
13+
814
// MARK: Properties
915

1016
/// The type of KDF used in the request.
@@ -40,6 +46,24 @@ struct KdfConfig: Codable, Equatable, Hashable {
4046
self.memory = memory
4147
self.parallelism = parallelism
4248
}
49+
50+
/// Initializes a `KdfConfig` from the SDK's `Kdf` type.
51+
///
52+
/// - Parameter kdf: The type of KDF used in the request.
53+
///
54+
init(kdf: Kdf) {
55+
switch kdf {
56+
case let .argon2id(iterations, memory, parallelism):
57+
self.init(
58+
kdfType: .argon2id,
59+
iterations: Int(iterations),
60+
memory: Int(memory),
61+
parallelism: Int(parallelism)
62+
)
63+
case let .pbkdf2(iterations):
64+
self.init(kdfType: .pbkdf2sha256, iterations: Int(iterations))
65+
}
66+
}
4367
}
4468

4569
// MARK: - KdfConfigProtocol
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import XCTest
2+
3+
@testable import BitwardenShared
4+
5+
class KdfConfigTests: BitwardenTestCase {
6+
// MARK: Tests
7+
8+
/// `init(kdf:)` initializes `KdfConfig` from `BitwardenSdk.Kdf` when using `argon2id`.
9+
func test_init_kdf_argon2() {
10+
let subject = KdfConfig(kdf: .argon2id(iterations: 3, memory: 64, parallelism: 4))
11+
XCTAssertEqual(
12+
subject,
13+
KdfConfig(
14+
kdfType: .argon2id,
15+
iterations: 3,
16+
memory: 64,
17+
parallelism: 4
18+
)
19+
)
20+
}
21+
22+
/// `init(kdf:)` initializes `KdfConfig` from `BitwardenSdk.Kdf` when using `pbkdf2`.
23+
func test_init_kdf_pbkdf2() {
24+
let subject = KdfConfig(kdf: .pbkdf2(iterations: 600_000))
25+
XCTAssertEqual(
26+
subject,
27+
KdfConfig(
28+
kdfType: .pbkdf2sha256,
29+
iterations: 600_000,
30+
memory: nil,
31+
parallelism: nil
32+
)
33+
)
34+
}
35+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import BitwardenSdk
2+
3+
// MARK: - MasterPasswordAuthenticationDataRequestModel
4+
5+
/// A request model for a user's master password authentication data.
6+
///
7+
struct MasterPasswordAuthenticationDataRequestModel: Encodable, Equatable {
8+
// MARK: Properties
9+
10+
/// The KDF settings.
11+
let kdf: KdfConfig
12+
13+
/// The master password hash.
14+
let masterPasswordAuthenticationHash: String
15+
16+
/// The salt used to compute the master password hash.
17+
let salt: String
18+
}
19+
20+
extension MasterPasswordAuthenticationDataRequestModel {
21+
/// Initialize `MasterPasswordAuthenticationDataRequestModel` from `MasterPasswordAuthenticationData`.
22+
///
23+
/// - Parameter authenticationData: The `MasterPasswordAuthenticationData` used to initialize a
24+
/// `MasterPasswordAuthenticationDataRequestModel`.
25+
///
26+
init(authenticationData: MasterPasswordAuthenticationData) {
27+
self.init(
28+
kdf: KdfConfig(kdf: authenticationData.kdf),
29+
masterPasswordAuthenticationHash: authenticationData.masterPasswordAuthenticationHash,
30+
salt: authenticationData.salt
31+
)
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import BitwardenSdk
2+
3+
// MARK: - MasterPasswordUnlockDataRequestModel
4+
5+
/// A request model for a user's unlock data.
6+
///
7+
struct MasterPasswordUnlockDataRequestModel: Encodable, Equatable {
8+
// MARK: Properties
9+
10+
/// The KDF settings.
11+
let kdf: KdfConfig
12+
13+
/// The user's master key encrypted with their user key.
14+
let masterKeyWrappedUserKey: String
15+
16+
/// The salt used to encrypt the user key.
17+
let salt: String
18+
}
19+
20+
extension MasterPasswordUnlockDataRequestModel {
21+
/// Initialize `MasterPasswordUnlockDataRequestModel` from `MasterPasswordUnlockData`.
22+
///
23+
/// - Parameter authenticationData: The `MasterPasswordUnlockData` used to initialize a
24+
/// `MasterPasswordUnlockDataRequestModel`.
25+
///
26+
init(unlockData: MasterPasswordUnlockData) {
27+
self.init(
28+
kdf: KdfConfig(kdf: unlockData.kdf),
29+
masterKeyWrappedUserKey: unlockData.masterKeyWrappedUserKey,
30+
salt: unlockData.salt
31+
)
32+
}
33+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import BitwardenSdk
2+
import Networking
3+
4+
// MARK: - UpdateKdfRequestModel
5+
6+
/// The request body for an update KDF request.
7+
///
8+
struct UpdateKdfRequestModel: JSONRequestBody, Equatable {
9+
// MARK: Properties
10+
11+
/// The user's data for authentication.
12+
let authenticationData: MasterPasswordAuthenticationDataRequestModel
13+
14+
/// The user's key.
15+
let key: String
16+
17+
/// The hash of the old master password.
18+
let masterPasswordHash: String
19+
20+
/// The hash of the new master password.
21+
let newMasterPasswordHash: String
22+
23+
/// The user's data for unlock.
24+
let unlockData: MasterPasswordUnlockDataRequestModel
25+
}
26+
27+
extension UpdateKdfRequestModel {
28+
/// Initialize `UpdateKdfRequestModel` from `UpdateKdfResponse`.
29+
///
30+
/// - Parameter response: The `UpdateKdfResponse` used to initialize a `UpdateKdfRequestModel`.
31+
///
32+
init(response: UpdateKdfResponse) {
33+
self.init(
34+
authenticationData: MasterPasswordAuthenticationDataRequestModel(
35+
authenticationData: response.masterPasswordAuthenticationData
36+
),
37+
key: response.masterPasswordUnlockData.masterKeyWrappedUserKey,
38+
masterPasswordHash: response.oldMasterPasswordAuthenticationData.masterPasswordAuthenticationHash,
39+
newMasterPasswordHash: response.masterPasswordAuthenticationData.masterPasswordAuthenticationHash,
40+
unlockData: MasterPasswordUnlockDataRequestModel(
41+
unlockData: response.masterPasswordUnlockData
42+
)
43+
)
44+
}
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import BitwardenSdk
2+
import XCTest
3+
4+
@testable import BitwardenShared
5+
6+
class UpdateKdfRequestModelTests: BitwardenTestCase {
7+
// MARK: Tests
8+
9+
/// `init(response:)` initializes `UpdateKdfRequestModel` from a `UpdateKdfResponse`.
10+
func test_init_response() {
11+
let subject = UpdateKdfRequestModel(
12+
response: UpdateKdfResponse(
13+
masterPasswordAuthenticationData: MasterPasswordAuthenticationData(
14+
kdf: .pbkdf2(iterations: 600_000),
15+
salt: "AUTHENTICATION_SALT",
16+
masterPasswordAuthenticationHash: "MASTER_PASSWORD_AUTHENTICATION_HASH"
17+
),
18+
masterPasswordUnlockData: MasterPasswordUnlockData(
19+
kdf: .argon2id(iterations: 3, memory: 64, parallelism: 4),
20+
masterKeyWrappedUserKey: "MASTER_KEY_WRAPPED_USER_KEY",
21+
salt: "UNLOCK_SALT"
22+
),
23+
oldMasterPasswordAuthenticationData: MasterPasswordAuthenticationData(
24+
kdf: .pbkdf2(iterations: 100_000),
25+
salt: "OLD_SALT",
26+
masterPasswordAuthenticationHash: "OLD_MASTER_PASSWORD_AUTHENTICATION_HASH"
27+
)
28+
)
29+
)
30+
XCTAssertEqual(
31+
subject,
32+
UpdateKdfRequestModel(
33+
authenticationData: MasterPasswordAuthenticationDataRequestModel(
34+
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: 600_000),
35+
masterPasswordAuthenticationHash: "MASTER_PASSWORD_AUTHENTICATION_HASH",
36+
salt: "AUTHENTICATION_SALT"
37+
),
38+
key: "MASTER_KEY_WRAPPED_USER_KEY",
39+
masterPasswordHash: "OLD_MASTER_PASSWORD_AUTHENTICATION_HASH",
40+
newMasterPasswordHash: "MASTER_PASSWORD_AUTHENTICATION_HASH",
41+
unlockData: MasterPasswordUnlockDataRequestModel(
42+
kdf: KdfConfig(kdfType: .argon2id, iterations: 3, memory: 64, parallelism: 4),
43+
masterKeyWrappedUserKey: "MASTER_KEY_WRAPPED_USER_KEY",
44+
salt: "UNLOCK_SALT"
45+
)
46+
)
47+
)
48+
}
49+
}

BitwardenShared/Core/Auth/Repositories/AuthRepository.swift

Lines changed: 21 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
@@ -1113,6 +1119,7 @@ extension DefaultAuthRepository: AuthRepository {
11131119
purpose: .localAuthorization
11141120
)
11151121
try await stateService.setMasterPasswordHash(hashedPassword)
1122+
await updateKdfToMinimumsIfNeeded(password: password)
11161123
case .decryptedKey,
11171124
.deviceKey,
11181125
.keyConnector,
@@ -1137,6 +1144,20 @@ extension DefaultAuthRepository: AuthRepository {
11371144
}
11381145
}
11391146

1147+
/// Updates the user's KDF settings to the minimums.
1148+
///
1149+
/// - Parameter password: The user's master password.
1150+
///
1151+
private func updateKdfToMinimumsIfNeeded(password: String) async {
1152+
do {
1153+
try await changeKdfService.updateKdfToMinimumsIfNeeded(password: password)
1154+
} catch {
1155+
// If an error occurs, log the error. Don't throw since that would block the vault from
1156+
// unlocking.
1157+
errorReporter.log(error: error)
1158+
}
1159+
}
1160+
11401161
func updateMasterPassword(
11411162
currentPassword: String,
11421163
newPassword: String,

0 commit comments

Comments
 (0)