mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-3565] Enforce higher minimum KDF (#6440)
Changes minimum iterations for PBKDF2 to 600 000. Also converts the constants into ranges to ensure there is only a single place for all checks.
This commit is contained in:
@@ -390,6 +390,14 @@ export abstract class CryptoService {
|
||||
privateKey: EncString;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Validate that the KDF config follows the requirements for the given KDF type.
|
||||
*
|
||||
* @remarks
|
||||
* Should always be called before updating a users KDF config.
|
||||
*/
|
||||
validateKdfConfig: (kdf: KdfType, kdfConfig: KdfConfig) => void;
|
||||
|
||||
/**
|
||||
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
|
||||
*/
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
import { RangeWithDefault } from "../misc/range-with-default";
|
||||
|
||||
export enum KdfType {
|
||||
PBKDF2_SHA256 = 0,
|
||||
Argon2id = 1,
|
||||
}
|
||||
|
||||
export const DEFAULT_ARGON2_MEMORY = 64;
|
||||
export const DEFAULT_ARGON2_PARALLELISM = 4;
|
||||
export const DEFAULT_ARGON2_ITERATIONS = 3;
|
||||
export const ARGON2_MEMORY = new RangeWithDefault(16, 1024, 64);
|
||||
export const ARGON2_PARALLELISM = new RangeWithDefault(1, 16, 4);
|
||||
export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3);
|
||||
|
||||
export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256;
|
||||
export const DEFAULT_PBKDF2_ITERATIONS = 600000;
|
||||
export const DEFAULT_KDF_CONFIG = new KdfConfig(DEFAULT_PBKDF2_ITERATIONS);
|
||||
export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000);
|
||||
export const DEFAULT_KDF_CONFIG = new KdfConfig(PBKDF2_ITERATIONS.defaultValue);
|
||||
|
||||
26
libs/common/src/platform/misc/range-with-default.spec.ts
Normal file
26
libs/common/src/platform/misc/range-with-default.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { RangeWithDefault } from "./range-with-default";
|
||||
|
||||
describe("RangeWithDefault", () => {
|
||||
describe("constructor", () => {
|
||||
it("should throw an error when min is greater than max", () => {
|
||||
expect(() => new RangeWithDefault(10, 5, 0)).toThrowError("10 is greater than 5.");
|
||||
});
|
||||
|
||||
it("should throw an error when default value is not in range", () => {
|
||||
expect(() => new RangeWithDefault(0, 10, 20)).toThrowError("Default value is not in range.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inRange", () => {
|
||||
it("should return true when in range", () => {
|
||||
const range = new RangeWithDefault(0, 10, 5);
|
||||
expect(range.inRange(5)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when not in range", () => {
|
||||
const range = new RangeWithDefault(5, 10, 7);
|
||||
expect(range.inRange(1)).toBe(false);
|
||||
expect(range.inRange(20)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
24
libs/common/src/platform/misc/range-with-default.ts
Normal file
24
libs/common/src/platform/misc/range-with-default.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* A range with a default value.
|
||||
*
|
||||
* Enforces constraints to ensure min > default > max.
|
||||
*/
|
||||
export class RangeWithDefault {
|
||||
constructor(
|
||||
readonly min: number,
|
||||
readonly max: number,
|
||||
readonly defaultValue: number,
|
||||
) {
|
||||
if (min > max) {
|
||||
throw new Error(`${min} is greater than ${max}.`);
|
||||
}
|
||||
|
||||
if (this.inRange(defaultValue) === false) {
|
||||
throw new Error("Default value is not in range.");
|
||||
}
|
||||
}
|
||||
|
||||
inRange(value: number): boolean {
|
||||
return value >= this.min && value <= this.max;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
KeySuffixOptions,
|
||||
HashPurpose,
|
||||
KdfType,
|
||||
DEFAULT_ARGON2_ITERATIONS,
|
||||
DEFAULT_ARGON2_MEMORY,
|
||||
DEFAULT_ARGON2_PARALLELISM,
|
||||
ARGON2_ITERATIONS,
|
||||
ARGON2_MEMORY,
|
||||
ARGON2_PARALLELISM,
|
||||
EncryptionType,
|
||||
PBKDF2_ITERATIONS,
|
||||
} from "../enums";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
import { EFFLongWordList } from "../misc/wordlist";
|
||||
@@ -175,6 +176,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a master key from a password and email.
|
||||
*
|
||||
* @remarks
|
||||
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
|
||||
*/
|
||||
async makeMasterKey(
|
||||
password: string,
|
||||
email: string,
|
||||
@@ -841,6 +848,43 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the KDF config follows the requirements for the given KDF type.
|
||||
*
|
||||
* @remarks
|
||||
* Should always be called before updating a users KDF config.
|
||||
*/
|
||||
validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void {
|
||||
switch (kdf) {
|
||||
case KdfType.PBKDF2_SHA256:
|
||||
if (!PBKDF2_ITERATIONS.inRange(kdfConfig.iterations)) {
|
||||
throw new Error(
|
||||
`PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case KdfType.Argon2id:
|
||||
if (!ARGON2_ITERATIONS.inRange(kdfConfig.iterations)) {
|
||||
throw new Error(
|
||||
`Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!ARGON2_MEMORY.inRange(kdfConfig.memory)) {
|
||||
throw new Error(
|
||||
`Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!ARGON2_PARALLELISM.inRange(kdfConfig.parallelism)) {
|
||||
throw new Error(
|
||||
`Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected async clearAllStoredUserKeys(userId?: string): Promise<void> {
|
||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||
@@ -900,30 +944,21 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
let key: Uint8Array = null;
|
||||
if (kdf == null || kdf === KdfType.PBKDF2_SHA256) {
|
||||
if (kdfConfig.iterations == null) {
|
||||
kdfConfig.iterations = 5000;
|
||||
} else if (kdfConfig.iterations < 5000) {
|
||||
throw new Error("PBKDF2 iteration minimum is 5000.");
|
||||
kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue;
|
||||
}
|
||||
|
||||
key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations);
|
||||
} else if (kdf == KdfType.Argon2id) {
|
||||
if (kdfConfig.iterations == null) {
|
||||
kdfConfig.iterations = DEFAULT_ARGON2_ITERATIONS;
|
||||
} else if (kdfConfig.iterations < 2) {
|
||||
throw new Error("Argon2 iteration minimum is 2.");
|
||||
kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue;
|
||||
}
|
||||
|
||||
if (kdfConfig.memory == null) {
|
||||
kdfConfig.memory = DEFAULT_ARGON2_MEMORY;
|
||||
} else if (kdfConfig.memory < 16) {
|
||||
throw new Error("Argon2 memory minimum is 16 MB");
|
||||
} else if (kdfConfig.memory > 1024) {
|
||||
throw new Error("Argon2 memory maximum is 1024 MB");
|
||||
kdfConfig.memory = ARGON2_MEMORY.defaultValue;
|
||||
}
|
||||
|
||||
if (kdfConfig.parallelism == null) {
|
||||
kdfConfig.parallelism = DEFAULT_ARGON2_PARALLELISM;
|
||||
} else if (kdfConfig.parallelism < 1) {
|
||||
throw new Error("Argon2 parallelism minimum is 1.");
|
||||
kdfConfig.parallelism = ARGON2_PARALLELISM.defaultValue;
|
||||
}
|
||||
|
||||
const saltHash = await this.cryptoFunctionService.hash(salt, "sha256");
|
||||
|
||||
Reference in New Issue
Block a user