1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[PM-5735] Create kdf Service (#8715)

* key connector migration initial

* migrator complete

* fix dependencies

* finalized tests

* fix deps and sync main

* clean up definition file

* fixing tests

* fixed tests

* fixing CLI, Browser, Desktop builds

* fixed factory options

* reverting exports

* implemented UserKeyDefinition clearOn

* Initial Kdf Service Changes

* rename and account setting kdfconfig

* fixing tests and renaming migration

* fixed DI ordering for browser

* rename and fix DI

* Clean up Migrations

* fixing migrations

* begin data structure changes for kdf config

* Make KDF more type safe; co-author: jlf0dev

* fixing tests

* Fixed CLI login and comments

* set now accepts userId and test updates

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
Ike
2024-04-25 11:26:01 -07:00
committed by GitHub
parent dba910d0b9
commit 1e4158fd87
82 changed files with 896 additions and 361 deletions

View File

@@ -0,0 +1,7 @@
import { UserId } from "../../types/guid";
import { KdfConfig } from "../models/domain/kdf-config";
export abstract class KdfConfigService {
setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise<void>;
getKdfConfig: () => Promise<KdfConfig>;
}

View File

@@ -1,11 +1,86 @@
export class KdfConfig {
iterations: number;
memory?: number;
parallelism?: number;
import { Jsonify } from "type-fest";
constructor(iterations: number, memory?: number, parallelism?: number) {
this.iterations = iterations;
this.memory = memory;
this.parallelism = parallelism;
import {
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
KdfType,
PBKDF2_ITERATIONS,
} from "../../../platform/enums/kdf-type.enum";
/**
* Represents a type safe KDF configuration.
*/
export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig;
/**
* Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration.
*/
export class PBKDF2KdfConfig {
kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256;
iterations: number;
constructor(iterations?: number) {
this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue;
}
/**
* Validates the PBKDF2 KDF configuration.
* A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000.
*/
validateKdfConfig(): void {
if (!PBKDF2_ITERATIONS.inRange(this.iterations)) {
throw new Error(
`PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`,
);
}
}
static fromJSON(json: Jsonify<PBKDF2KdfConfig>): PBKDF2KdfConfig {
return new PBKDF2KdfConfig(json.iterations);
}
}
/**
* Argon2 KDF configuration.
*/
export class Argon2KdfConfig {
kdfType: KdfType.Argon2id = KdfType.Argon2id;
iterations: number;
memory: number;
parallelism: number;
constructor(iterations?: number, memory?: number, parallelism?: number) {
this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue;
this.memory = memory ?? ARGON2_MEMORY.defaultValue;
this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue;
}
/**
* Validates the Argon2 KDF configuration.
* A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16.
*/
validateKdfConfig(): void {
if (!ARGON2_ITERATIONS.inRange(this.iterations)) {
throw new Error(
`Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`,
);
}
if (!ARGON2_MEMORY.inRange(this.memory)) {
throw new Error(
`Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`,
);
}
if (!ARGON2_PARALLELISM.inRange(this.parallelism)) {
throw new Error(
`Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`,
);
}
}
static fromJSON(json: Jsonify<Argon2KdfConfig>): Argon2KdfConfig {
return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism);
}
}

View File

@@ -11,18 +11,14 @@ export class SetKeyConnectorKeyRequest {
kdfParallelism?: number;
orgIdentifier: string;
constructor(
key: string,
kdf: KdfType,
kdfConfig: KdfConfig,
orgIdentifier: string,
keys: KeysRequest,
) {
constructor(key: string, kdfConfig: KdfConfig, orgIdentifier: string, keys: KeysRequest) {
this.key = key;
this.kdf = kdf;
this.kdf = kdfConfig.kdfType;
this.kdfIterations = kdfConfig.iterations;
this.kdfMemory = kdfConfig.memory;
this.kdfParallelism = kdfConfig.parallelism;
if (kdfConfig.kdfType === KdfType.Argon2id) {
this.kdfMemory = kdfConfig.memory;
this.kdfParallelism = kdfConfig.parallelism;
}
this.orgIdentifier = orgIdentifier;
this.keys = keys;
}

View File

@@ -0,0 +1,104 @@
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import {
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
PBKDF2_ITERATIONS,
} from "../../platform/enums/kdf-type.enum";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config";
import { KdfConfigService } from "./kdf-config.service";
describe("KdfConfigService", () => {
let sutKdfConfigService: KdfConfigService;
let fakeStateProvider: FakeStateProvider;
let fakeAccountService: FakeAccountService;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => {
jest.clearAllMocks();
fakeAccountService = mockAccountServiceWith(mockUserId);
fakeStateProvider = new FakeStateProvider(fakeAccountService);
sutKdfConfigService = new KdfConfigService(fakeStateProvider);
});
it("setKdfConfig(): should set the KDF config", async () => {
const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000);
await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig);
await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig);
});
it("setKdfConfig(): should get the KDF config", async () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4);
await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig);
await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig);
});
it("setKdfConfig(): should throw error KDF cannot be null", async () => {
const kdfConfig: Argon2KdfConfig = null;
try {
await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig);
} catch (e) {
expect(e).toEqual(new Error("kdfConfig cannot be null"));
}
});
it("setKdfConfig(): should throw error userId cannot be null", async () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4);
try {
await sutKdfConfigService.setKdfConfig(null, kdfConfig);
} catch (e) {
expect(e).toEqual(new Error("userId cannot be null"));
}
});
it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => {
try {
await sutKdfConfigService.getKdfConfig();
} catch (e) {
expect(e).toEqual(new Error("KdfConfig for active user account state is null"));
}
});
it("validateKdfConfig(): should validate the PBKDF2 KDF config", () => {
const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000);
expect(() => kdfConfig.validateKdfConfig()).not.toThrow();
});
it("validateKdfConfig(): should validate the Argon2id KDF config", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4);
expect(() => kdfConfig.validateKdfConfig()).not.toThrow();
});
it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => {
const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`,
);
});
it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`,
);
});
it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`,
);
});
it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`,
);
});
});

View File

@@ -0,0 +1,41 @@
import { firstValueFrom } from "rxjs";
import { KdfType } from "../../platform/enums/kdf-type.enum";
import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
import { KdfConfigService as KdfConfigServiceAbstraction } from "../abstractions/kdf-config.service";
import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config";
export const KDF_CONFIG = new UserKeyDefinition<KdfConfig>(KDF_CONFIG_DISK, "kdfConfig", {
deserializer: (kdfConfig: KdfConfig) => {
if (kdfConfig == null) {
return null;
}
return kdfConfig.kdfType === KdfType.PBKDF2_SHA256
? PBKDF2KdfConfig.fromJSON(kdfConfig)
: Argon2KdfConfig.fromJSON(kdfConfig);
},
clearOn: ["logout"],
});
export class KdfConfigService implements KdfConfigServiceAbstraction {
constructor(private stateProvider: StateProvider) {}
async setKdfConfig(userId: UserId, kdfConfig: KdfConfig) {
if (!userId) {
throw new Error("userId cannot be null");
}
if (kdfConfig === null) {
throw new Error("kdfConfig cannot be null");
}
await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId);
}
async getKdfConfig(): Promise<KdfConfig> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$);
if (state === null) {
throw new Error("KdfConfig for active user account state is null");
}
return state;
}
}

View File

@@ -7,6 +7,7 @@ import { KeysRequest } from "../../models/request/keys.request";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { KdfType } from "../../platform/enums/kdf-type.enum";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import {
@@ -20,7 +21,7 @@ import { AccountService } from "../abstractions/account.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { KdfConfig } from "../models/domain/kdf-config";
import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config";
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
@@ -133,12 +134,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
userDecryptionOptions,
} = tokenResponse;
const password = await this.keyGenerationService.createKey(512);
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const kdfConfig: KdfConfig =
kdf === KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(kdfIterations)
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const masterKey = await this.cryptoService.makeMasterKey(
password.keyB64,
await this.tokenService.getEmail(),
kdf,
kdfConfig,
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
@@ -162,7 +165,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetKeyConnectorKeyRequest(
userKey[1].encryptedString,
kdf,
kdfConfig,
orgId,
keys,

View File

@@ -13,6 +13,7 @@ import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enu
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { AccountService } from "../../abstractions/account.service";
import { KdfConfigService } from "../../abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
@@ -47,6 +48,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private kdfConfigService: KdfConfigService,
) {}
async getAvailableVerificationOptions(
@@ -118,8 +120,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
masterKey = await this.cryptoService.makeMasterKey(
verification.secret,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig(),
await this.kdfConfigService.getKdfConfig(),
);
}
request.masterPasswordHash = alreadyHashed
@@ -176,8 +177,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
masterKey = await this.cryptoService.makeMasterKey(
verification.secret,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig(),
await this.kdfConfigService.getKdfConfig(),
);
}
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(