diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 295037ce6b5..2249c83ebce 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -61,7 +61,7 @@ export class ChangeKdfConfirmationComponent { const masterPassword = this.form.value.masterPassword; // Ensure the KDF config is valid. - this.kdfConfig.validateKdfConfig(); + this.kdfConfig.validateKdfConfigForSetting(); const request = new KdfRequest(); request.kdf = this.kdfConfig.kdfType; diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 778ad7c74c2..14662bb4b89 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -11,9 +11,11 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service" import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; +import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; +import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogin.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; @@ -159,6 +161,9 @@ describe("LoginStrategyService", () => { new IdentityTokenResponse({ ForcePasswordReset: false, Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, Key: "KEY", PrivateKey: "PRIVATE_KEY", ResetMasterPassword: false, @@ -169,6 +174,15 @@ describe("LoginStrategyService", () => { token_type: "Bearer", }), ); + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + tokenService.decodeAccessToken.calledWith("ACCESS_TOKEN").mockResolvedValue({ sub: "USER_ID", name: "NAME", @@ -194,6 +208,15 @@ describe("LoginStrategyService", () => { }), ); + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + await sut.logIn(credentials); const twoFactorToken = new TokenTwoFactorRequest( @@ -205,6 +228,9 @@ describe("LoginStrategyService", () => { new IdentityTokenResponse({ ForcePasswordReset: false, Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, Key: "KEY", PrivateKey: "PRIVATE_KEY", ResetMasterPassword: false, @@ -241,6 +267,15 @@ describe("LoginStrategyService", () => { }), ); + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.Argon2id, + KdfIterations: 2, + KdfMemory: 16, + KdfParallelism: 1, + }), + ); + await sut.logIn(credentials); loginStrategyCacheExpirationState.stateSubject.next(new Date(Date.now() - 1000 * 60 * 5)); @@ -253,4 +288,40 @@ describe("LoginStrategyService", () => { await expect(sut.logInTwoFactor(twoFactorToken, "CAPTCHA")).rejects.toThrow(); }); + + it("throw error on too low kdf config", async () => { + const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD"); + apiService.postIdentityToken.mockResolvedValue( + new IdentityTokenResponse({ + ForcePasswordReset: false, + Kdf: KdfType.PBKDF2_SHA256, + KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min - 1, + Key: "KEY", + PrivateKey: "PRIVATE_KEY", + ResetMasterPassword: false, + access_token: "ACCESS_TOKEN", + expires_in: 3600, + refresh_token: "REFRESH_TOKEN", + scope: "api offline_access", + token_type: "Bearer", + }), + ); + apiService.postPrelogin.mockResolvedValue( + new PreloginResponse({ + Kdf: KdfType.PBKDF2_SHA256, + KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min - 1, + }), + ); + + tokenService.decodeAccessToken.calledWith("ACCESS_TOKEN").mockResolvedValue({ + sub: "USER_ID", + name: "NAME", + email: "EMAIL", + premium: false, + }); + + await expect(sut.logIn(credentials)).rejects.toThrow( + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min} and ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.max}`, + ); + }); }); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 89c2bc01d94..35f2b90bbda 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -264,6 +264,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { throw e; } } + + kdfConfig.validateKdfConfigForPrelogin(); + return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); } diff --git a/libs/common/src/auth/models/domain/kdf-config.ts b/libs/common/src/auth/models/domain/kdf-config.ts index 7378081550b..908b187e30c 100644 --- a/libs/common/src/auth/models/domain/kdf-config.ts +++ b/libs/common/src/auth/models/domain/kdf-config.ts @@ -13,6 +13,7 @@ export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; */ export class PBKDF2KdfConfig { static ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); + static PRELOGIN_ITERATIONS = new RangeWithDefault(5000, 2_000_000, 600_000); kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256; iterations: number; @@ -21,10 +22,10 @@ export class PBKDF2KdfConfig { } /** - * Validates the PBKDF2 KDF configuration. + * Validates the PBKDF2 KDF configuration for updating the KDF config. * A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000. */ - validateKdfConfig(): void { + validateKdfConfigForSetting(): void { if (!PBKDF2KdfConfig.ITERATIONS.inRange(this.iterations)) { throw new Error( `PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`, @@ -32,6 +33,18 @@ export class PBKDF2KdfConfig { } } + /** + * Validates the PBKDF2 KDF configuration for pre-login. + * A Valid PBKDF2 KDF configuration has KDF iterations between the 5000 and 2_000_000. + */ + validateKdfConfigForPrelogin(): void { + if (!PBKDF2KdfConfig.PRELOGIN_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min} and ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.max}`, + ); + } + } + static fromJSON(json: Jsonify): PBKDF2KdfConfig { return new PBKDF2KdfConfig(json.iterations); } @@ -44,6 +57,11 @@ export class Argon2KdfConfig { static MEMORY = new RangeWithDefault(16, 1024, 64); static PARALLELISM = new RangeWithDefault(1, 16, 4); static ITERATIONS = new RangeWithDefault(2, 10, 3); + + static PRELOGIN_MEMORY = Argon2KdfConfig.MEMORY; + static PRELOGIN_PARALLELISM = Argon2KdfConfig.PARALLELISM; + static PRELOGIN_ITERATIONS = Argon2KdfConfig.ITERATIONS; + kdfType: KdfType.Argon2id = KdfType.Argon2id; iterations: number; memory: number; @@ -56,10 +74,10 @@ export class Argon2KdfConfig { } /** - * Validates the Argon2 KDF configuration. + * Validates the Argon2 KDF configuration for updating the KDF config. * A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16. */ - validateKdfConfig(): void { + validateKdfConfigForSetting(): void { if (!Argon2KdfConfig.ITERATIONS.inRange(this.iterations)) { throw new Error( `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, @@ -79,6 +97,29 @@ export class Argon2KdfConfig { } } + /** + * Validates the Argon2 KDF configuration for pre-login. + */ + validateKdfConfigForPrelogin(): void { + if (!Argon2KdfConfig.PRELOGIN_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `Argon2 iterations must be between ${Argon2KdfConfig.PRELOGIN_ITERATIONS.min} and ${Argon2KdfConfig.PRELOGIN_ITERATIONS.max}`, + ); + } + + if (!Argon2KdfConfig.PRELOGIN_MEMORY.inRange(this.memory)) { + throw new Error( + `Argon2 memory must be between ${Argon2KdfConfig.PRELOGIN_MEMORY.min}mb and ${Argon2KdfConfig.PRELOGIN_MEMORY.max}mb`, + ); + } + + if (!Argon2KdfConfig.PRELOGIN_PARALLELISM.inRange(this.parallelism)) { + throw new Error( + `Argon2 parallelism must be between ${Argon2KdfConfig.PRELOGIN_PARALLELISM.min} and ${Argon2KdfConfig.PRELOGIN_PARALLELISM.max}.`, + ); + } + } + static fromJSON(json: Jsonify): Argon2KdfConfig { return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } diff --git a/libs/common/src/auth/services/kdf-config.service.spec.ts b/libs/common/src/auth/services/kdf-config.service.spec.ts index 7f8357ffb55..968b0cbd8ff 100644 --- a/libs/common/src/auth/services/kdf-config.service.spec.ts +++ b/libs/common/src/auth/services/kdf-config.service.spec.ts @@ -58,41 +58,120 @@ describe("KdfConfigService", () => { } }); - it("validateKdfConfig(): should validate the PBKDF2 KDF config", () => { + it("validateKdfConfigForSetting(): should validate the PBKDF2 KDF config", () => { const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); - expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + expect(() => kdfConfig.validateKdfConfigForSetting()).not.toThrow(); }); - it("validateKdfConfig(): should validate the Argon2id KDF config", () => { + it("validateKdfConfigForSetting(): should validate the Argon2id KDF config", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); - expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + expect(() => kdfConfig.validateKdfConfigForSetting()).not.toThrow(); }); - it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => { - const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100); - expect(() => kdfConfig.validateKdfConfig()).toThrow( + it("validateKdfConfigForSetting(): should throw an error for invalid PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100000); + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( `PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`, ); }); - it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => { + it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 iterations", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); - expect(() => kdfConfig.validateKdfConfig()).toThrow( + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, ); }); - it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => { + it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 memory", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4); - expect(() => kdfConfig.validateKdfConfig()).toThrow( + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( `Argon2 memory must be between ${Argon2KdfConfig.MEMORY.min}mb and ${Argon2KdfConfig.MEMORY.max}mb`, ); }); - it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => { + it("validateKdfConfigForSetting(): should throw an error for invalid Argon2 parallelism", () => { const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); - expect(() => kdfConfig.validateKdfConfig()).toThrow( + expect(() => kdfConfig.validateKdfConfigForSetting()).toThrow( `Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}`, ); }); + + it("validateKdfConfigForPrelogin(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + }); + + it("validateKdfConfigForPrelogin(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow(); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too low PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig( + PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min - 1, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min} and ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too high PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig( + PBKDF2KdfConfig.PRELOGIN_ITERATIONS.max + 1, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.min} and ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too low Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( + Argon2KdfConfig.ITERATIONS.min - 1, + 64, + 4, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too high Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( + Argon2KdfConfig.PRELOGIN_ITERATIONS.max + 1, + 64, + 4, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too low Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( + 3, + Argon2KdfConfig.PRELOGIN_MEMORY.min - 1, + 4, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 memory must be between ${Argon2KdfConfig.PRELOGIN_MEMORY.min}mb and ${Argon2KdfConfig.PRELOGIN_MEMORY.max}mb`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too high Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig( + 3, + Argon2KdfConfig.PRELOGIN_MEMORY.max + 1, + 4, + ); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 memory must be between ${Argon2KdfConfig.PRELOGIN_MEMORY.min}mb and ${Argon2KdfConfig.PRELOGIN_MEMORY.max}mb`, + ); + }); + + it("validateKdfConfigForPrelogin(): should throw an error for too high Argon2 parallelism", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); + expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow( + `Argon2 parallelism must be between ${Argon2KdfConfig.PRELOGIN_PARALLELISM.min} and ${Argon2KdfConfig.PRELOGIN_PARALLELISM.max}`, + ); + }); });