Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1287,3 +1287,7 @@
"WarningStartsWithIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "**Warning:** โ€œStarts withโ€ is an advanced option with increased risk of exposing credentials.";
"WarningRegularExpressionIsAnAdvancedOptionWithIncreasedRiskOfExposingCredentials" = "**Warning:** โ€œRegular expressionโ€ is an advanced option with increased risk of exposing credentials if used incorrectly.";
"DefaultX" = "Default (%1$@)";
"UpdateYourEncryptionSettings" = "Update your encryption settings";
"TheNewRecommendedEncryptionSettingsDescriptionLong" = "The new recommended encryption settings will improve your account security. Enter your master password to update now.";
"Updating" = "Updating...";

Choose a reason for hiding this comment

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

Should be Updatingโ€ฆ

Last time someone unify all ... (3 dots) to โ€ฆ character

"EncryptionSettingsUpdated" = "Encryption settings updated";
24 changes: 24 additions & 0 deletions BitwardenShared/Core/Auth/Models/Domains/KdfConfig.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import BitwardenKit
import BitwardenSdk

// MARK: - KdfConfig

/// A model for configuring KDF options.
///
struct KdfConfig: Codable, Equatable, Hashable {
// MARK: Type Properties

/// The default `KdfConfig` used for new accounts or when upgrading the KDF config to minimums.
static let defaultKdfConfig = KdfConfig(kdfType: .pbkdf2sha256, iterations: Constants.pbkdf2Iterations)

// MARK: Properties

/// The type of KDF used in the request.
Expand Down Expand Up @@ -40,6 +46,24 @@ struct KdfConfig: Codable, Equatable, Hashable {
self.memory = memory
self.parallelism = parallelism
}

/// Initializes a `KdfConfig` from the SDK's `Kdf` type.
///
/// - Parameter kdf: The type of KDF used in the request.
///
init(kdf: Kdf) {
switch kdf {
case let .argon2id(iterations, memory, parallelism):
self.init(
kdfType: .argon2id,
iterations: Int(iterations),
memory: Int(memory),
parallelism: Int(parallelism)
)
case let .pbkdf2(iterations):
self.init(kdfType: .pbkdf2sha256, iterations: Int(iterations))
}
}
}

// MARK: - KdfConfigProtocol
Expand Down
35 changes: 35 additions & 0 deletions BitwardenShared/Core/Auth/Models/Domains/KdfConfigTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import XCTest

@testable import BitwardenShared

class KdfConfigTests: BitwardenTestCase {
// MARK: Tests

/// `init(kdf:)` initializes `KdfConfig` from `BitwardenSdk.Kdf` when using `argon2id`.
func test_init_kdf_argon2() {
let subject = KdfConfig(kdf: .argon2id(iterations: 3, memory: 64, parallelism: 4))
XCTAssertEqual(
subject,
KdfConfig(
kdfType: .argon2id,
iterations: 3,
memory: 64,
parallelism: 4
)
)
}

/// `init(kdf:)` initializes `KdfConfig` from `BitwardenSdk.Kdf` when using `pbkdf2`.
func test_init_kdf_pbkdf2() {
let subject = KdfConfig(kdf: .pbkdf2(iterations: 600_000))
XCTAssertEqual(
subject,
KdfConfig(
kdfType: .pbkdf2sha256,
iterations: 600_000,
memory: nil,
parallelism: nil
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import BitwardenSdk

// MARK: - MasterPasswordAuthenticationDataRequestModel

/// A request model for a user's master password authentication data.
///
struct MasterPasswordAuthenticationDataRequestModel: Encodable, Equatable {
// MARK: Properties

/// The KDF settings.
let kdf: KdfConfig

/// The master password hash.
let masterPasswordAuthenticationHash: String

/// The salt used to compute the master password hash.
let salt: String
}

extension MasterPasswordAuthenticationDataRequestModel {
/// Initialize `MasterPasswordAuthenticationDataRequestModel` from `MasterPasswordAuthenticationData`.
///
/// - Parameter authenticationData: The `MasterPasswordAuthenticationData` used to initialize a
/// `MasterPasswordAuthenticationDataRequestModel`.
///
init(authenticationData: MasterPasswordAuthenticationData) {
self.init(
kdf: KdfConfig(kdf: authenticationData.kdf),
masterPasswordAuthenticationHash: authenticationData.masterPasswordAuthenticationHash,
salt: authenticationData.salt
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import BitwardenSdk

// MARK: - MasterPasswordUnlockDataRequestModel

/// A request model for a user's unlock data.
///
struct MasterPasswordUnlockDataRequestModel: Encodable, Equatable {
// MARK: Properties

/// The KDF settings.
let kdf: KdfConfig

/// The user's master key encrypted with their user key.
let masterKeyWrappedUserKey: String

/// The salt used to encrypt the user key.
let salt: String
}

extension MasterPasswordUnlockDataRequestModel {
/// Initialize `MasterPasswordUnlockDataRequestModel` from `MasterPasswordUnlockData`.
///
/// - Parameter authenticationData: The `MasterPasswordUnlockData` used to initialize a
/// `MasterPasswordUnlockDataRequestModel`.
///
init(unlockData: MasterPasswordUnlockData) {
self.init(
kdf: KdfConfig(kdf: unlockData.kdf),
masterKeyWrappedUserKey: unlockData.masterKeyWrappedUserKey,
salt: unlockData.salt
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import BitwardenSdk
import Networking

// MARK: - UpdateKdfRequestModel

/// The request body for an update KDF request.
///
struct UpdateKdfRequestModel: JSONRequestBody, Equatable {
// MARK: Properties

/// The user's data for authentication.
let authenticationData: MasterPasswordAuthenticationDataRequestModel

/// The user's key.
let key: String

/// The hash of the old master password.
let masterPasswordHash: String

/// The hash of the new master password.
let newMasterPasswordHash: String

/// The user's data for unlock.
let unlockData: MasterPasswordUnlockDataRequestModel
}

extension UpdateKdfRequestModel {
/// Initialize `UpdateKdfRequestModel` from `UpdateKdfResponse`.
///
/// - Parameter response: The `UpdateKdfResponse` used to initialize a `UpdateKdfRequestModel`.
///
init(response: UpdateKdfResponse) {
self.init(
authenticationData: MasterPasswordAuthenticationDataRequestModel(
authenticationData: response.masterPasswordAuthenticationData
),
key: response.masterPasswordUnlockData.masterKeyWrappedUserKey,
masterPasswordHash: response.oldMasterPasswordAuthenticationData.masterPasswordAuthenticationHash,
newMasterPasswordHash: response.masterPasswordAuthenticationData.masterPasswordAuthenticationHash,
unlockData: MasterPasswordUnlockDataRequestModel(
unlockData: response.masterPasswordUnlockData
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import BitwardenSdk
import XCTest

@testable import BitwardenShared

class UpdateKdfRequestModelTests: BitwardenTestCase {
// MARK: Tests

/// `init(response:)` initializes `UpdateKdfRequestModel` from a `UpdateKdfResponse`.
func test_init_response() {
let subject = UpdateKdfRequestModel(
response: UpdateKdfResponse(
masterPasswordAuthenticationData: MasterPasswordAuthenticationData(
kdf: .pbkdf2(iterations: 600_000),
salt: "AUTHENTICATION_SALT",
masterPasswordAuthenticationHash: "MASTER_PASSWORD_AUTHENTICATION_HASH"
),
masterPasswordUnlockData: MasterPasswordUnlockData(
kdf: .argon2id(iterations: 3, memory: 64, parallelism: 4),
masterKeyWrappedUserKey: "MASTER_KEY_WRAPPED_USER_KEY",
salt: "UNLOCK_SALT"
),
oldMasterPasswordAuthenticationData: MasterPasswordAuthenticationData(
kdf: .pbkdf2(iterations: 100_000),
salt: "OLD_SALT",
masterPasswordAuthenticationHash: "OLD_MASTER_PASSWORD_AUTHENTICATION_HASH"
)
)
)
XCTAssertEqual(
subject,
UpdateKdfRequestModel(
authenticationData: MasterPasswordAuthenticationDataRequestModel(
kdf: KdfConfig(kdfType: .pbkdf2sha256, iterations: 600_000),
masterPasswordAuthenticationHash: "MASTER_PASSWORD_AUTHENTICATION_HASH",
salt: "AUTHENTICATION_SALT"
),
key: "MASTER_KEY_WRAPPED_USER_KEY",
masterPasswordHash: "OLD_MASTER_PASSWORD_AUTHENTICATION_HASH",
newMasterPasswordHash: "MASTER_PASSWORD_AUTHENTICATION_HASH",
unlockData: MasterPasswordUnlockDataRequestModel(
kdf: KdfConfig(kdfType: .argon2id, iterations: 3, memory: 64, parallelism: 4),
masterKeyWrappedUserKey: "MASTER_KEY_WRAPPED_USER_KEY",
salt: "UNLOCK_SALT"
)
)
)
}
}
21 changes: 21 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ class DefaultAuthRepository {
/// The service to use system Biometrics for vault unlock.
let biometricsRepository: BiometricsRepository

/// The service used to change the user's KDF settings.
private let changeKdfService: ChangeKdfService

/// The service that handles common client functionality such as encryption and decryption.
private let clientService: ClientService

Expand Down Expand Up @@ -468,6 +471,7 @@ class DefaultAuthRepository {
/// - appContextHelper: The helper to know about the app context.
/// - authService: The service used that handles some of the auth logic.
/// - biometricsRepository: The service to use system Biometrics for vault unlock.
/// - changeKdfService: The service used to change the user's KDF settings.
/// - clientService: The service that handles common client functionality such as encryption and decryption.
/// - configService: The service to get server-specified configuration.
/// - environmentService: The service used by the application to manage the environment settings.
Expand All @@ -488,6 +492,7 @@ class DefaultAuthRepository {
appContextHelper: AppContextHelper,
authService: AuthService,
biometricsRepository: BiometricsRepository,
changeKdfService: ChangeKdfService,
clientService: ClientService,
configService: ConfigService,
environmentService: EnvironmentService,
Expand All @@ -506,6 +511,7 @@ class DefaultAuthRepository {
self.appContextHelper = appContextHelper
self.authService = authService
self.biometricsRepository = biometricsRepository
self.changeKdfService = changeKdfService
self.clientService = clientService
self.configService = configService
self.environmentService = environmentService
Expand Down Expand Up @@ -1113,6 +1119,7 @@ extension DefaultAuthRepository: AuthRepository {
purpose: .localAuthorization
)
try await stateService.setMasterPasswordHash(hashedPassword)
await updateKdfToMinimumsIfNeeded(password: password)
case .decryptedKey,
.deviceKey,
.keyConnector,
Expand All @@ -1137,6 +1144,20 @@ extension DefaultAuthRepository: AuthRepository {
}
}

/// Updates the user's KDF settings to the minimums.
///
/// - Parameter password: The user's master password.
///
private func updateKdfToMinimumsIfNeeded(password: String) async {
do {
try await changeKdfService.updateKdfToMinimumsIfNeeded(password: password)
} catch {
// If an error occurs, log the error. Don't throw since that would block the vault from
// unlocking.
errorReporter.log(error: error)
}
}

func updateMasterPassword(
currentPassword: String,
newPassword: String,
Expand Down
Loading