mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +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:
7
libs/common/src/auth/abstractions/kdf-config.service.ts
Normal file
7
libs/common/src/auth/abstractions/kdf-config.service.ts
Normal 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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
104
libs/common/src/auth/services/kdf-config.service.spec.ts
Normal file
104
libs/common/src/auth/services/kdf-config.service.spec.ts
Normal 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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
41
libs/common/src/auth/services/kdf-config.service.ts
Normal file
41
libs/common/src/auth/services/kdf-config.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -55,6 +55,7 @@ import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-maste
|
||||
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
|
||||
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
|
||||
import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag";
|
||||
import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
@@ -62,7 +63,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 58;
|
||||
export const CURRENT_VERSION = 59;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -122,7 +123,8 @@ export function createMigrationBuilder() {
|
||||
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
|
||||
.with(AuthRequestMigrator, 55, 56)
|
||||
.with(CipherServiceMigrator, 56, 57)
|
||||
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION);
|
||||
.with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58)
|
||||
.with(KdfConfigMigrator, 58, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { KdfConfigMigrator } from "./59-move-kdf-config-to-state-provider";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
FirstAccount: {
|
||||
profile: {
|
||||
kdfIterations: 3,
|
||||
kdfMemory: 64,
|
||||
kdfParallelism: 5,
|
||||
kdfType: 1,
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
SecondAccount: {
|
||||
profile: {
|
||||
kdfIterations: 600_001,
|
||||
kdfMemory: null as number,
|
||||
kdfParallelism: null as number,
|
||||
kdfType: 0,
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
user_FirstAccount_kdfConfig_kdfConfig: {
|
||||
iterations: 3,
|
||||
memory: 64,
|
||||
parallelism: 5,
|
||||
kdfType: 1,
|
||||
},
|
||||
user_SecondAccount_kdfConfig_kdfConfig: {
|
||||
iterations: 600_001,
|
||||
memory: null as number,
|
||||
parallelism: null as number,
|
||||
kdfType: 0,
|
||||
},
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
FirstAccount: {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
profile: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const kdfConfigKeyDefinition: KeyDefinitionLike = {
|
||||
key: "kdfConfig",
|
||||
stateDefinition: {
|
||||
name: "kdfConfig",
|
||||
},
|
||||
};
|
||||
|
||||
describe("KdfConfigMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: KdfConfigMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 59);
|
||||
sut = new KdfConfigMigrator(58, 59);
|
||||
});
|
||||
|
||||
it("should remove kdfType and kdfConfig from Account.Profile", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
});
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, {
|
||||
iterations: 3,
|
||||
memory: 64,
|
||||
parallelism: 5,
|
||||
kdfType: 1,
|
||||
});
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, {
|
||||
iterations: 600_001,
|
||||
memory: null as number,
|
||||
parallelism: null as number,
|
||||
kdfType: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 59);
|
||||
sut = new KdfConfigMigrator(58, 59);
|
||||
});
|
||||
|
||||
it("should null out new KdfConfig account value and set account.profile", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(2);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, null);
|
||||
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
profile: {
|
||||
kdfIterations: 3,
|
||||
kdfMemory: 64,
|
||||
kdfParallelism: 5,
|
||||
kdfType: 1,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("SecondAccount", {
|
||||
profile: {
|
||||
kdfIterations: 600_001,
|
||||
kdfMemory: null as number,
|
||||
kdfParallelism: null as number,
|
||||
kdfType: 0,
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
enum KdfType {
|
||||
PBKDF2_SHA256 = 0,
|
||||
Argon2id = 1,
|
||||
}
|
||||
|
||||
class KdfConfig {
|
||||
iterations: number;
|
||||
kdfType: KdfType;
|
||||
memory?: number;
|
||||
parallelism?: number;
|
||||
}
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
kdfIterations: number;
|
||||
kdfType: KdfType;
|
||||
kdfMemory?: number;
|
||||
kdfParallelism?: number;
|
||||
};
|
||||
};
|
||||
|
||||
const kdfConfigKeyDefinition: KeyDefinitionLike = {
|
||||
key: "kdfConfig",
|
||||
stateDefinition: {
|
||||
name: "kdfConfig",
|
||||
},
|
||||
};
|
||||
|
||||
export class KdfConfigMigrator extends Migrator<58, 59> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const iterations = account?.profile?.kdfIterations;
|
||||
const kdfType = account?.profile?.kdfType;
|
||||
const memory = account?.profile?.kdfMemory;
|
||||
const parallelism = account?.profile?.kdfParallelism;
|
||||
|
||||
const kdfConfig: KdfConfig = {
|
||||
iterations: iterations,
|
||||
kdfType: kdfType,
|
||||
memory: memory,
|
||||
parallelism: parallelism,
|
||||
};
|
||||
|
||||
if (kdfConfig != null) {
|
||||
await helper.setToUser(userId, kdfConfigKeyDefinition, kdfConfig);
|
||||
delete account?.profile?.kdfIterations;
|
||||
delete account?.profile?.kdfType;
|
||||
delete account?.profile?.kdfMemory;
|
||||
delete account?.profile?.kdfParallelism;
|
||||
}
|
||||
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
const kdfConfig: KdfConfig = await helper.getFromUser(userId, kdfConfigKeyDefinition);
|
||||
|
||||
if (kdfConfig != null) {
|
||||
account.profile.kdfIterations = kdfConfig.iterations;
|
||||
account.profile.kdfType = kdfConfig.kdfType;
|
||||
account.profile.kdfMemory = kdfConfig.memory;
|
||||
account.profile.kdfParallelism = kdfConfig.parallelism;
|
||||
await helper.setToUser(userId, kdfConfigKeyDefinition, null);
|
||||
}
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { KdfType } from "../../../platform/enums";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
@@ -69,8 +69,7 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
const passwordKey = await this.keyGenerationService.deriveKeyFromPassword(
|
||||
password,
|
||||
model.key,
|
||||
KdfType.PBKDF2_SHA256,
|
||||
{ iterations: SEND_KDF_ITERATIONS },
|
||||
new PBKDF2KdfConfig(SEND_KDF_ITERATIONS),
|
||||
);
|
||||
send.password = passwordKey.keyB64;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user