1
0
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:
Bernd Schoolmann
2025-08-15 23:49:49 +02:00
committed by GitHub
parent d4bb341847
commit a60b7fed9a
21 changed files with 236 additions and 198 deletions

View File

@@ -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";

View File

@@ -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.",
);
});
});
});

View File

@@ -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";