1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +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

@@ -0,0 +1,2 @@
export { KeyGenerationService } from "./key-generation/key-generation.service";
export { DefaultKeyGenerationService } from "./key-generation/default-key-generation.service";

View File

@@ -0,0 +1,94 @@
// 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 { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { EncryptionType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KeyGenerationService } from "./key-generation.service";
export class DefaultKeyGenerationService implements KeyGenerationService {
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);
}
}

View File

@@ -4,21 +4,21 @@ import { mock } from "jest-mock-extended";
// 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 { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { EncryptionType } from "../../../platform/enums";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KeyGenerationService } from "./key-generation.service";
import { DefaultKeyGenerationService } from "./default-key-generation.service";
describe("KeyGenerationService", () => {
let sut: KeyGenerationService;
let sut: DefaultKeyGenerationService;
const cryptoFunctionService = mock<CryptoFunctionService>();
beforeEach(() => {
sut = new KeyGenerationService(cryptoFunctionService);
sut = new DefaultKeyGenerationService(cryptoFunctionService);
});
describe("createKey", () => {

View File

@@ -0,0 +1,90 @@
// 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 { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
/**
* @deprecated This is a low-level cryptographic service. New functionality should not be built
* on top of it, and instead should be built in the sdk.
*/
export abstract class KeyGenerationService {
/**
* Generates a key of the given length suitable for use in AES encryption
*
* @deprecated WARNING: DO NOT USE THIS FOR NEW CODE. Direct generation and handling of keys should only be done in the SDK,
* as memory safety cannot be ensured in a JS context.
*
* @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.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @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.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @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.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @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.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @param key 32 byte key.
* @returns 64 byte derived key.
*/
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
}

View File

@@ -20,7 +20,6 @@ import {
import { AppIdService } from "../../../platform/abstractions/app-id.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
@@ -30,6 +29,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { UserKey, DeviceKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";

View File

@@ -25,7 +25,6 @@ import { DeviceType } from "../../../enums";
import { AppIdService } from "../../../platform/abstractions/app-id.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
@@ -37,6 +36,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
import { CsprngArray } from "../../../types/csprng";
import { UserId } from "../../../types/guid";
import { DeviceKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";

View File

@@ -18,9 +18,9 @@ import { TokenService } from "../../../auth/services/token.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { KeyGenerationService } from "../../../platform/services/key-generation.service";
import { OrganizationId, UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { EncString } from "../../crypto/models/enc-string";
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";

View File

@@ -23,13 +23,13 @@ import { Organization } from "../../../admin-console/models/domain/organization"
import { TokenService } from "../../../auth/abstractions/token.service";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { KeysRequest } from "../../../models/request/keys.request";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { KEY_CONNECTOR_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";

View File

@@ -13,13 +13,13 @@ import {
mockAccountServiceWith,
} from "../../../../spec";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";

View File

@@ -11,7 +11,6 @@ import { KdfConfig } from "@bitwarden/key-management";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncryptionType } from "../../../platform/enums";
@@ -24,6 +23,7 @@ import {
} from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../../crypto/models/enc-string";

View File

@@ -9,11 +9,11 @@ import { AccountService } from "../../auth/abstractions/account.service";
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { EncString, EncryptedString } from "../../key-management/crypto/models/enc-string";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { PIN_DISK, PIN_MEMORY, StateProvider, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { KeyGenerationService } from "../crypto";
import { PinServiceAbstraction } from "./pin.service.abstraction";

View File

@@ -4,12 +4,12 @@ import { mock } from "jest-mock-extended";
import { DEFAULT_KDF_CONFIG, KdfConfigService } from "@bitwarden/key-management";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { KeyGenerationService } from "../crypto";
import { CryptoFunctionService } from "../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../crypto/abstractions/encrypt.service";
import { EncString } from "../crypto/models/enc-string";

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