mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
[PM-17667] Move key-generation service to KM ownership (#16015)
* Move key-generation service * Update comment * Add deprecation comments * Fix firefox build * Update comment * Update DI import * Update module imports
This commit is contained in:
@@ -1,66 +1,2 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class KeyGenerationService {
|
||||
/**
|
||||
* Generates a key of the given length suitable for use in AES encryption
|
||||
* @param bitLength Length of key.
|
||||
* 256 bits = 32 bytes
|
||||
* 512 bits = 64 bytes
|
||||
* @returns Generated key.
|
||||
*/
|
||||
abstract createKey(bitLength: 256 | 512): Promise<SymmetricCryptoKey>;
|
||||
/**
|
||||
* Generates key material from CSPRNG and derives a 64 byte key from it.
|
||||
* Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869}
|
||||
* for details.
|
||||
* @param bitLength Length of key material.
|
||||
* @param purpose Purpose for the key derivation function.
|
||||
* Different purposes results in different keys, even with the same material.
|
||||
* @param salt Optional. If not provided will be generated from CSPRNG.
|
||||
* @returns An object containing the salt, key material, and derived key.
|
||||
*/
|
||||
abstract createKeyWithPurpose(
|
||||
bitLength: 128 | 192 | 256 | 512,
|
||||
purpose: string,
|
||||
salt?: string,
|
||||
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>;
|
||||
/**
|
||||
* Derives a 64 byte key from key material.
|
||||
* @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}.
|
||||
* Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} for details.
|
||||
* @param material key material.
|
||||
* @param salt Salt for the key derivation function.
|
||||
* @param purpose Purpose for the key derivation function.
|
||||
* Different purposes results in different keys, even with the same material.
|
||||
* @returns 64 byte derived key.
|
||||
*/
|
||||
abstract deriveKeyFromMaterial(
|
||||
material: CsprngArray,
|
||||
salt: string,
|
||||
purpose: string,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
/**
|
||||
* 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 kdfConfig Configuration for the key derivation function.
|
||||
* @returns 32 byte derived key.
|
||||
*/
|
||||
abstract deriveKeyFromPassword(
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
|
||||
/**
|
||||
* Derives a 64 byte key from a 32 byte key using a key derivation function.
|
||||
* @param key 32 byte key.
|
||||
* @returns 64 byte derived key.
|
||||
*/
|
||||
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
|
||||
}
|
||||
/** Temporary re-export. This should not be used for new imports */
|
||||
export { KeyGenerationService } from "../../key-management/crypto/key-generation/key-generation.service";
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PBKDF2KdfConfig, Argon2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { SdkLoadService } from "../abstractions/sdk/sdk-load.service";
|
||||
import { EncryptionType } from "../enums";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { KeyGenerationService } from "./key-generation.service";
|
||||
|
||||
describe("KeyGenerationService", () => {
|
||||
let sut: KeyGenerationService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new KeyGenerationService(cryptoFunctionService);
|
||||
});
|
||||
|
||||
describe("createKey", () => {
|
||||
test.each([256, 512])(
|
||||
"it should delegate key creation to crypto function service",
|
||||
async (bitLength: 256 | 512) => {
|
||||
cryptoFunctionService.aesGenerateKey
|
||||
.calledWith(bitLength)
|
||||
.mockResolvedValue(new Uint8Array(bitLength / 8) as CsprngArray);
|
||||
|
||||
await sut.createKey(bitLength);
|
||||
|
||||
expect(cryptoFunctionService.aesGenerateKey).toHaveBeenCalledWith(bitLength);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("createMaterialAndKey", () => {
|
||||
test.each([128, 192, 256, 512])(
|
||||
"should create a 64 byte key from different material lengths",
|
||||
async (bitLength: 128 | 192 | 256 | 512) => {
|
||||
const inputMaterial = new Uint8Array(bitLength / 8) as CsprngArray;
|
||||
const inputSalt = "salt";
|
||||
const purpose = "purpose";
|
||||
|
||||
cryptoFunctionService.aesGenerateKey.calledWith(bitLength).mockResolvedValue(inputMaterial);
|
||||
cryptoFunctionService.hkdf
|
||||
.calledWith(inputMaterial, inputSalt, purpose, 64, "sha256")
|
||||
.mockResolvedValue(new Uint8Array(64));
|
||||
|
||||
const { salt, material, derivedKey } = await sut.createKeyWithPurpose(
|
||||
bitLength,
|
||||
purpose,
|
||||
inputSalt,
|
||||
);
|
||||
|
||||
expect(salt).toEqual(inputSalt);
|
||||
expect(material).toEqual(inputMaterial);
|
||||
expect(derivedKey.inner().type).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("deriveKeyFromMaterial", () => {
|
||||
it("should derive a 64 byte key from material", async () => {
|
||||
const material = new Uint8Array(32) as CsprngArray;
|
||||
const salt = "salt";
|
||||
const purpose = "purpose";
|
||||
|
||||
cryptoFunctionService.hkdf.mockResolvedValue(new Uint8Array(64));
|
||||
|
||||
const key = await sut.deriveKeyFromMaterial(material, salt, purpose);
|
||||
|
||||
expect(key.inner().type).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveKeyFromPassword", () => {
|
||||
it("should derive a 32 byte key from a password using pbkdf2", async () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
const salt = new Uint8Array([9, 10, 11, 12]);
|
||||
const kdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
|
||||
Object.defineProperty(SdkLoadService, "Ready", {
|
||||
value: Promise.resolve(),
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig);
|
||||
expect(key.inner().type).toEqual(EncryptionType.AesCbc256_B64);
|
||||
});
|
||||
|
||||
it("should derive a 32 byte key from a password using argon2id", async () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
const salt = new Uint8Array([9, 10, 11, 12]);
|
||||
const kdfConfig = new Argon2KdfConfig(3, 16, 4);
|
||||
|
||||
Object.defineProperty(SdkLoadService, "Ready", {
|
||||
value: Promise.resolve(),
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig);
|
||||
expect(key.inner().type).toEqual(EncryptionType.AesCbc256_B64);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stretchKey", () => {
|
||||
it("should stretch a key", async () => {
|
||||
const key = new SymmetricCryptoKey(new Uint8Array(32));
|
||||
|
||||
cryptoFunctionService.hkdf.mockResolvedValue(new Uint8Array(64));
|
||||
|
||||
const stretchedKey = await sut.stretchKey(key);
|
||||
|
||||
expect(stretchedKey.inner().type).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
});
|
||||
it("should throw if key is not 32 bytes", async () => {
|
||||
const key = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
|
||||
await expect(sut.stretchKey(key)).rejects.toThrow(
|
||||
"Key passed into stretchKey is not a 256-bit key.",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,92 +1,2 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "../abstractions/key-generation.service";
|
||||
import { SdkLoadService } from "../abstractions/sdk/sdk-load.service";
|
||||
import { EncryptionType } from "../enums";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export class KeyGenerationService implements KeyGenerationServiceAbstraction {
|
||||
constructor(private cryptoFunctionService: CryptoFunctionService) {}
|
||||
|
||||
async createKey(bitLength: 256 | 512): Promise<SymmetricCryptoKey> {
|
||||
const key = await this.cryptoFunctionService.aesGenerateKey(bitLength);
|
||||
return new SymmetricCryptoKey(key);
|
||||
}
|
||||
|
||||
async createKeyWithPurpose(
|
||||
bitLength: 128 | 192 | 256 | 512,
|
||||
purpose: string,
|
||||
salt?: string,
|
||||
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }> {
|
||||
if (salt == null) {
|
||||
const bytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
salt = Utils.fromBufferToUtf8(bytes);
|
||||
}
|
||||
const material = await this.cryptoFunctionService.aesGenerateKey(bitLength);
|
||||
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
|
||||
return { salt, material, derivedKey: new SymmetricCryptoKey(key) };
|
||||
}
|
||||
|
||||
async deriveKeyFromMaterial(
|
||||
material: CsprngArray,
|
||||
salt: string,
|
||||
purpose: string,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
|
||||
return new SymmetricCryptoKey(key);
|
||||
}
|
||||
|
||||
async deriveKeyFromPassword(
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
if (typeof password === "string") {
|
||||
password = new TextEncoder().encode(password);
|
||||
}
|
||||
if (typeof salt === "string") {
|
||||
salt = new TextEncoder().encode(salt);
|
||||
}
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
return new SymmetricCryptoKey(
|
||||
PureCrypto.derive_kdf_material(password, salt, kdfConfig.toSdkConfig()),
|
||||
);
|
||||
}
|
||||
|
||||
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
// The key to be stretched is actually usually the output of a KDF, and not actually meant for AesCbc256_B64 encryption,
|
||||
// but has the same key length. Only 256-bit key materials should be stretched.
|
||||
if (key.inner().type != EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Key passed into stretchKey is not a 256-bit key.");
|
||||
}
|
||||
|
||||
const newKey = new Uint8Array(64);
|
||||
// Master key and pin key are always 32 bytes
|
||||
const encKey = await this.cryptoFunctionService.hkdfExpand(
|
||||
key.inner().encryptionKey,
|
||||
"enc",
|
||||
32,
|
||||
"sha256",
|
||||
);
|
||||
const macKey = await this.cryptoFunctionService.hkdfExpand(
|
||||
key.inner().encryptionKey,
|
||||
"mac",
|
||||
32,
|
||||
"sha256",
|
||||
);
|
||||
|
||||
newKey.set(new Uint8Array(encKey));
|
||||
newKey.set(new Uint8Array(macKey), 32);
|
||||
|
||||
return new SymmetricCryptoKey(newKey);
|
||||
}
|
||||
}
|
||||
/** Temporary re-export. This should not be used for new imports */
|
||||
export { DefaultKeyGenerationService as KeyGenerationService } from "../../key-management/crypto/key-generation/default-key-generation.service";
|
||||
|
||||
Reference in New Issue
Block a user