1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-04 01:23:57 +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,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(