1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +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

@@ -6,7 +6,7 @@ import { ProfileProviderResponse } from "../../admin-console/models/response/pro
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key";
import { KeySuffixOptions, KdfType, HashPurpose } from "../enums";
import { KeySuffixOptions, HashPurpose } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
@@ -114,16 +114,10 @@ export abstract class CryptoService {
* Generates a master key from the provided password
* @param password The user's master password
* @param email The user's email
* @param kdf The user's selected key derivation function to use
* @param KdfConfig The user's key derivation function configuration
* @returns A master key derived from the provided password
*/
abstract makeMasterKey(
password: string,
email: string,
kdf: KdfType,
KdfConfig: KdfConfig,
): Promise<MasterKey>;
abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey>;
/**
* Encrypts the existing (or provided) user key with the
* provided master key
@@ -258,16 +252,10 @@ export abstract class CryptoService {
/**
* @param pin The user's pin
* @param salt The user's salt
* @param kdf The user's kdf
* @param kdfConfig The user's kdf config
* @returns A key derived from the user's pin
*/
abstract makePinKey(
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
): Promise<PinKey>;
abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>;
/**
* Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result,
@@ -279,7 +267,6 @@ export abstract class CryptoService {
* Decrypts the user key with their pin
* @param pin The user's PIN
* @param salt The user's salt
* @param kdf The user's KDF
* @param kdfConfig The user's KDF config
* @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
* it will be retrieved from storage
@@ -288,7 +275,6 @@ export abstract class CryptoService {
abstract decryptUserKeyWithPin(
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString,
): Promise<UserKey>;
@@ -298,7 +284,6 @@ export abstract class CryptoService {
* @param masterPasswordOnRestart True if Master Password on Restart is enabled
* @param pin User's PIN
* @param email User's email
* @param kdf User's KdfType
* @param kdfConfig User's KdfConfig
* @param oldPinKey The old Pin key from state (retrieved from different
* places depending on if Master Password on Restart was enabled)
@@ -308,7 +293,6 @@ export abstract class CryptoService {
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdf: KdfType,
kdfConfig: KdfConfig,
oldPinKey: EncString,
): Promise<UserKey>;
@@ -358,21 +342,12 @@ 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.
*/
abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void;
/**
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
*/
abstract decryptMasterKeyWithPin(
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString,
): Promise<MasterKey>;

View File

@@ -1,6 +1,5 @@
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { CsprngArray } from "../../types/csprng";
import { KdfType } from "../enums";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class KeyGenerationService {
@@ -46,14 +45,12 @@ export abstract class KeyGenerationService {
* Derives a 32 byte key from a password using a key derivation function.
* @param password Password to derive the key from.
* @param salt Salt for the key derivation function.
* @param kdf Key derivation function to use.
* @param kdfConfig Configuration for the key derivation function.
* @returns 32 byte derived key.
*/
abstract deriveKeyFromPassword(
password: string | Uint8Array,
salt: string | Uint8Array,
kdf: KdfType,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>;
}

View File

@@ -1,12 +1,10 @@
import { Observable } from "rxjs";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key";
import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { UserId } from "../../types/guid";
import { KdfType } from "../enums";
import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options";
@@ -149,10 +147,6 @@ export abstract class StateService<T extends Account = Account> {
*/
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>;
setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>;
getKdfType: (options?: StorageOptions) => Promise<KdfType>;
setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>;
getLastActive: (options?: StorageOptions) => Promise<number>;
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
getLastSync: (options?: StorageOptions) => Promise<string>;

View File

@@ -1,4 +1,4 @@
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config";
import { RangeWithDefault } from "../misc/range-with-default";
export enum KdfType {
@@ -12,4 +12,4 @@ export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3);
export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256;
export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000);
export const DEFAULT_KDF_CONFIG = new KdfConfig(PBKDF2_ITERATIONS.defaultValue);
export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue);

View File

@@ -4,6 +4,7 @@ import { firstValueFrom, of, tap } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid";
@@ -37,6 +38,7 @@ describe("cryptoService", () => {
const platformUtilService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
const stateService = mock<StateService>();
const kdfConfigService = mock<KdfConfigService>();
let stateProvider: FakeStateProvider;
const mockUserId = Utils.newGuid() as UserId;
@@ -58,6 +60,7 @@ describe("cryptoService", () => {
stateService,
accountService,
stateProvider,
kdfConfigService,
);
});

View File

@@ -6,6 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { AccountService } from "../../auth/abstractions/account.service";
import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Utils } from "../../platform/misc/utils";
@@ -28,16 +29,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import {
KeySuffixOptions,
HashPurpose,
KdfType,
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
EncryptionType,
PBKDF2_ITERATIONS,
} from "../enums";
import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums";
import { sequentialize } from "../misc/sequentialize";
import { EFFLongWordList } from "../misc/wordlist";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
@@ -91,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction {
protected stateService: StateService,
protected accountService: AccountService,
protected stateProvider: StateProvider,
protected kdfConfigService: KdfConfigService,
) {
// User Key
this.activeUserKeyState = stateProvider.getActive(USER_KEY);
@@ -283,8 +276,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return (masterKey ||= await this.makeMasterKey(
password,
await this.stateService.getEmail({ userId: userId }),
await this.stateService.getKdfType({ userId: userId }),
await this.stateService.getKdfConfig({ userId: userId }),
await this.kdfConfigService.getKdfConfig(),
));
}
@@ -295,16 +287,10 @@ export class CryptoService implements CryptoServiceAbstraction {
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
* TODO: Move to MasterPasswordService
*/
async makeMasterKey(
password: string,
email: string,
kdf: KdfType,
KdfConfig: KdfConfig,
): Promise<MasterKey> {
async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey> {
return (await this.keyGenerationService.deriveKeyFromPassword(
password,
email,
kdf,
KdfConfig,
)) as MasterKey;
}
@@ -560,8 +546,8 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
}
async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise<PinKey> {
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdf, kdfConfig);
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
return (await this.stretchKey(pinKey)) as PinKey;
}
@@ -575,7 +561,6 @@ export class CryptoService implements CryptoServiceAbstraction {
async decryptUserKeyWithPin(
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
pinProtectedUserKey?: EncString,
): Promise<UserKey> {
@@ -584,7 +569,7 @@ export class CryptoService implements CryptoServiceAbstraction {
if (!pinProtectedUserKey) {
throw new Error("No PIN protected key found.");
}
const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig);
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey);
return new SymmetricCryptoKey(userKey) as UserKey;
}
@@ -593,7 +578,6 @@ export class CryptoService implements CryptoServiceAbstraction {
async decryptMasterKeyWithPin(
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
pinProtectedMasterKey?: EncString,
): Promise<MasterKey> {
@@ -604,7 +588,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString);
}
const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig);
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey);
return new SymmetricCryptoKey(masterKey) as MasterKey;
}
@@ -831,8 +815,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const pinKey = await this.makePinKey(
pin,
await this.stateService.getEmail({ userId: userId }),
await this.stateService.getKdfType({ userId: userId }),
await this.stateService.getKdfConfig({ userId: userId }),
await this.kdfConfigService.getKdfConfig(),
);
const encPin = await this.encryptService.encrypt(key.key, pinKey);
@@ -873,43 +856,6 @@ 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?: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
@@ -1007,16 +953,15 @@ export class CryptoService implements CryptoServiceAbstraction {
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdf: KdfType,
kdfConfig: KdfConfig,
oldPinKey: EncString,
): Promise<UserKey> {
// Decrypt
const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdf, kdfConfig, oldPinKey);
const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey);
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey));
// Migrate
const pinKey = await this.makePinKey(pin, email, kdf, kdfConfig);
const pinKey = await this.makePinKey(pin, email, kdfConfig);
const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey);
if (masterPasswordOnRestart) {
await this.stateService.setDecryptedPinProtected(null);

View File

@@ -1,9 +1,8 @@
import { mock } from "jest-mock-extended";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Argon2KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KdfType } from "../enums";
import { KeyGenerationService } from "./key-generation.service";
@@ -75,12 +74,11 @@ describe("KeyGenerationService", () => {
it("should derive a 32 byte key from a password using pbkdf2", async () => {
const password = "password";
const salt = "salt";
const kdf = KdfType.PBKDF2_SHA256;
const kdfConfig = new KdfConfig(600_000);
const kdfConfig = new PBKDF2KdfConfig(600_000);
cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32));
const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig);
const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig);
expect(key.key.length).toEqual(32);
});
@@ -88,13 +86,12 @@ describe("KeyGenerationService", () => {
it("should derive a 32 byte key from a password using argon2id", async () => {
const password = "password";
const salt = "salt";
const kdf = KdfType.Argon2id;
const kdfConfig = new KdfConfig(600_000, 15);
const kdfConfig = new Argon2KdfConfig(3, 16, 4);
cryptoFunctionService.hash.mockResolvedValue(new Uint8Array(32));
cryptoFunctionService.argon2.mockResolvedValue(new Uint8Array(32));
const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig);
const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig);
expect(key.key.length).toEqual(32);
});

View File

@@ -46,17 +46,16 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
async deriveKeyFromPassword(
password: string | Uint8Array,
salt: string | Uint8Array,
kdf: KdfType,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey> {
let key: Uint8Array = null;
if (kdf == null || kdf === KdfType.PBKDF2_SHA256) {
if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue;
}
key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations);
} else if (kdf == KdfType.Argon2id) {
} else if (kdfConfig.kdfType == KdfType.Argon2id) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue;
}

View File

@@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest";
import { AccountService } from "../../auth/abstractions/account.service";
import { TokenService } from "../../auth/abstractions/token.service";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key";
import { GeneratorOptions } from "../../tools/generator/generator-options";
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
@@ -19,7 +18,7 @@ import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums";
import { HtmlStorageLocation, StorageLocation } from "../enums";
import { StateFactory } from "../factories/state-factory";
import { Utils } from "../misc/utils";
import { Account, AccountData, AccountSettings } from "../models/domain/account";
@@ -643,49 +642,6 @@ export class StateService<
);
}
async getKdfConfig(options?: StorageOptions): Promise<KdfConfig> {
const iterations = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.profile?.kdfIterations;
const memory = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.profile?.kdfMemory;
const parallelism = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.profile?.kdfParallelism;
return new KdfConfig(iterations, memory, parallelism);
}
async setKdfConfig(config: KdfConfig, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.profile.kdfIterations = config.iterations;
account.profile.kdfMemory = config.memory;
account.profile.kdfParallelism = config.parallelism;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getKdfType(options?: StorageOptions): Promise<KdfType> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.profile?.kdfType;
}
async setKdfType(value: KdfType, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.profile.kdfType = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getLastActive(options?: StorageOptions): Promise<number> {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());

View File

@@ -35,6 +35,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
// Auth
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");