1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-6211] Create key generation service (#7939)

* create key generation service

* replace old key generation service and add references

* use key generation service in key connector service

* use key generation service in send service

* user key generation service in access service

* use key generation service in device trust service

* fix tests

* fix browser

* add createKeyFromMaterial and tests

* create ephemeral key

* fix tests

* rename method and add returns docs

* ignore material in destructure

* modify test

* specify material as key material

* pull out magic strings to properties

* make salt optional and generate if not provided

* fix test

* fix parameters

* update docs to include link to HKDF rfc
This commit is contained in:
Jake Fink
2024-02-23 08:48:15 -05:00
committed by GitHub
parent 071959317c
commit 19a373d87e
27 changed files with 401 additions and 149 deletions

View File

@@ -102,6 +102,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -125,6 +126,7 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
@@ -431,10 +433,16 @@ import { ModalService } from "./modal.service";
deps: [CryptoFunctionServiceAbstraction, LogService, StateServiceAbstraction],
},
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
{
provide: KeyGenerationServiceAbstraction,
useClass: KeyGenerationService,
deps: [CryptoFunctionServiceAbstraction],
},
{
provide: CryptoServiceAbstraction,
useClass: CryptoService,
deps: [
KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction,
EncryptService,
PlatformUtilsServiceAbstraction,
@@ -476,7 +484,7 @@ import { ModalService } from "./modal.service";
deps: [
CryptoServiceAbstraction,
I18nServiceAbstraction,
CryptoFunctionServiceAbstraction,
KeyGenerationServiceAbstraction,
StateServiceAbstraction,
],
},
@@ -683,7 +691,7 @@ import { ModalService } from "./modal.service";
TokenServiceAbstraction,
LogService,
OrganizationServiceAbstraction,
CryptoFunctionServiceAbstraction,
KeyGenerationServiceAbstraction,
LOGOUT_CALLBACK,
],
},
@@ -825,6 +833,7 @@ import { ModalService } from "./modal.service";
provide: DeviceTrustCryptoServiceAbstraction,
useClass: DeviceTrustCryptoService,
deps: [
KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction,
CryptoServiceAbstraction,
EncryptService,

View File

@@ -5,11 +5,11 @@ import { CryptoFunctionService } from "../../platform/abstractions/crypto-functi
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 { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { UserKey, DeviceKey } from "../../types/key";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
@@ -22,6 +22,7 @@ import {
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
constructor(
private keyGenerationService: KeyGenerationService,
private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
@@ -165,10 +166,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
private async makeDeviceKey(): Promise<DeviceKey> {
// Create 512-bit device key
const randomBytes: CsprngArray = await this.cryptoFunctionService.aesGenerateKey(512);
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
return deviceKey;
return (await this.keyGenerationService.createKey(512)) as DeviceKey;
}
async decryptUserKeyWithDeviceKey(

View File

@@ -7,6 +7,7 @@ import { CryptoFunctionService } from "../../platform/abstractions/crypto-functi
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 { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncryptionType } from "../../platform/enums/encryption-type.enum";
@@ -24,6 +25,7 @@ import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implemen
describe("deviceTrustCryptoService", () => {
let deviceTrustCryptoService: DeviceTrustCryptoService;
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
@@ -37,6 +39,7 @@ describe("deviceTrustCryptoService", () => {
jest.clearAllMocks();
deviceTrustCryptoService = new DeviceTrustCryptoService(
keyGenerationService,
cryptoFunctionService,
cryptoService,
encryptService,
@@ -166,17 +169,18 @@ describe("deviceTrustCryptoService", () => {
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
const mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes) as DeviceKey;
const cryptoFuncSvcGenerateKeySpy = jest
.spyOn(cryptoFunctionService, "aesGenerateKey")
.mockResolvedValue(mockRandomBytes);
const keyGenSvcGenerateKeySpy = jest
.spyOn(keyGenerationService, "createKey")
.mockResolvedValue(mockDeviceKey);
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey();
expect(cryptoFuncSvcGenerateKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoFuncSvcGenerateKeySpy).toHaveBeenCalledWith(deviceKeyBytesLength * 8);
expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledTimes(1);
expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledWith(deviceKeyBytesLength * 8);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);

View File

@@ -2,8 +2,8 @@ import { ApiService } from "../../abstractions/api.service";
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "../../admin-console/enums";
import { KeysRequest } from "../../models/request/keys.request";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
@@ -24,7 +24,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
private tokenService: TokenService,
private logService: LogService,
private organizationService: OrganizationService,
private cryptoFunctionService: CryptoFunctionService,
private keyGenerationService: KeyGenerationService,
private logoutCallback: (expired: boolean, userId?: string) => Promise<void>,
) {}
@@ -94,11 +94,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
keyConnectorUrl: legacyKeyConnectorUrl,
userDecryptionOptions,
} = tokenResponse;
const password = await this.cryptoFunctionService.aesGenerateKey(512);
const password = await this.keyGenerationService.createKey(512);
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const masterKey = await this.cryptoService.makeMasterKey(
Utils.fromBufferToB64(password),
password.keyB64,
await this.tokenService.getEmail(),
kdf,
kdfConfig,

View File

@@ -0,0 +1,59 @@
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 {
/**
* 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.
*/
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.
*/
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.
*/
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 kdf Key derivation function to use.
* @param kdfConfig Configuration for the key derivation function.
* @returns 32 byte derived key.
*/
deriveKeyFromPassword: (
password: string | Uint8Array,
salt: string | Uint8Array,
kdf: KdfType,
kdfConfig: KdfConfig,
) => Promise<SymmetricCryptoKey>;
}

View File

@@ -10,6 +10,7 @@ import { UserId } from "../../types/guid";
import { UserKey, MasterKey, PinKey } from "../../types/key";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { EncryptService } from "../abstractions/encrypt.service";
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";
@@ -23,6 +24,7 @@ import { USER_EVER_HAD_USER_KEY, USER_KEY } from "./key-state/user-key.state";
describe("cryptoService", () => {
let cryptoService: CryptoService;
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
const platformUtilService = mock<PlatformUtilsService>();
@@ -38,6 +40,7 @@ describe("cryptoService", () => {
stateProvider = new FakeStateProvider(accountService);
cryptoService = new CryptoService(
keyGenerationService,
cryptoFunctionService,
encryptService,
platformUtilService,

View File

@@ -9,6 +9,7 @@ import { AccountService } from "../../auth/abstractions/account.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Utils } from "../../platform/misc/utils";
import { CsprngArray } from "../../types/csprng";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import {
OrgKey,
@@ -23,6 +24,7 @@ import {
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
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";
@@ -80,6 +82,7 @@ export class CryptoService implements CryptoServiceAbstraction {
readonly everHadUserKey$: Observable<boolean>;
constructor(
protected keyGenerationService: KeyGenerationService,
protected cryptoFunctionService: CryptoFunctionService,
protected encryptService: EncryptService,
protected platformUtilService: PlatformUtilsService,
@@ -219,8 +222,8 @@ export class CryptoService implements CryptoServiceAbstraction {
throw new Error("No Master Key found.");
}
const newUserKey = await this.cryptoFunctionService.aesGenerateKey(512);
return this.buildProtectedSymmetricKey(masterKey, newUserKey);
const newUserKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(masterKey, newUserKey.key);
}
async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise<void> {
@@ -294,7 +297,12 @@ export class CryptoService implements CryptoServiceAbstraction {
kdf: KdfType,
KdfConfig: KdfConfig,
): Promise<MasterKey> {
return (await this.makeKey(password, email, kdf, KdfConfig)) as MasterKey;
return (await this.keyGenerationService.deriveKeyFromPassword(
password,
email,
kdf,
KdfConfig,
)) as MasterKey;
}
async clearMasterKey(userId?: UserId): Promise<void> {
@@ -452,8 +460,8 @@ export class CryptoService implements CryptoServiceAbstraction {
throw new Error("No key provided");
}
const newSymKey = await this.cryptoFunctionService.aesGenerateKey(512);
return this.buildProtectedSymmetricKey(key, newSymKey);
const newSymKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(key, newSymKey.key);
}
async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> {
@@ -522,10 +530,10 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]> {
const shareKey = await this.cryptoFunctionService.aesGenerateKey(512);
const shareKey = await this.keyGenerationService.createKey(512);
const publicKey = await this.getPublicKey();
const encShareKey = await this.rsaEncrypt(shareKey, publicKey);
return [encShareKey, new SymmetricCryptoKey(shareKey) as T];
const encShareKey = await this.rsaEncrypt(shareKey.key, publicKey);
return [encShareKey, shareKey as T];
}
async setPrivateKey(encPrivateKey: EncryptedString): Promise<void> {
@@ -588,7 +596,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise<PinKey> {
const pinKey = await this.makeKey(pin, salt, kdf, kdfConfig);
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdf, kdfConfig);
return (await this.stretchKey(pinKey)) as PinKey;
}
@@ -636,20 +644,16 @@ export class CryptoService implements CryptoServiceAbstraction {
return new SymmetricCryptoKey(masterKey) as MasterKey;
}
async makeSendKey(keyMaterial: Uint8Array): Promise<SymmetricCryptoKey> {
const sendKey = await this.cryptoFunctionService.hkdf(
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
return await this.keyGenerationService.deriveKeyFromMaterial(
keyMaterial,
"bitwarden-send",
"send",
64,
"sha256",
);
return new SymmetricCryptoKey(sendKey);
}
async makeCipherKey(): Promise<CipherKey> {
const randomBytes = await this.cryptoFunctionService.aesGenerateKey(512);
return new SymmetricCryptoKey(randomBytes) as CipherKey;
return (await this.keyGenerationService.createKey(512)) as CipherKey;
}
async clearKeys(userId?: UserId): Promise<any> {
@@ -802,8 +806,7 @@ export class CryptoService implements CryptoServiceAbstraction {
publicKey: string;
privateKey: EncString;
}> {
const rawKey = await this.cryptoFunctionService.aesGenerateKey(512);
const userKey = new SymmetricCryptoKey(rawKey) as UserKey;
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
await this.setUserKey(userKey);
await this.activeUserEncryptedPrivateKeyState.update(() => privateKey.encryptedString);
@@ -986,46 +989,6 @@ export class CryptoService implements CryptoServiceAbstraction {
return [new SymmetricCryptoKey(newSymKey) as T, protectedSymKey];
}
private async makeKey(
password: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey> {
let key: Uint8Array = null;
if (kdf == null || kdf === 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) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue;
}
if (kdfConfig.memory == null) {
kdfConfig.memory = ARGON2_MEMORY.defaultValue;
}
if (kdfConfig.parallelism == null) {
kdfConfig.parallelism = ARGON2_PARALLELISM.defaultValue;
}
const saltHash = await this.cryptoFunctionService.hash(salt, "sha256");
key = await this.cryptoFunctionService.argon2(
password,
saltHash,
kdfConfig.iterations,
kdfConfig.memory * 1024, // convert to KiB from MiB
kdfConfig.parallelism,
);
} else {
throw new Error("Unknown Kdf.");
}
return new SymmetricCryptoKey(key);
}
// --LEGACY METHODS--
// We previously used the master key for additional keys, but now we use the user key.
// These methods support migrating the old keys to the new ones.

View File

@@ -0,0 +1,102 @@
import { mock } from "jest-mock-extended";
import { KdfConfig } 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";
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.key.length).toEqual(64);
},
);
});
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.key.length).toEqual(64);
});
});
describe("deriveKeyFromPassword", () => {
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);
cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32));
const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig);
expect(key.key.length).toEqual(32);
});
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);
cryptoFunctionService.hash.mockResolvedValue(new Uint8Array(32));
cryptoFunctionService.argon2.mockResolvedValue(new Uint8Array(32));
const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig);
expect(key.key.length).toEqual(32);
});
});
});

View File

@@ -0,0 +1,85 @@
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "../abstractions/key-generation.service";
import {
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
KdfType,
PBKDF2_ITERATIONS,
} 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,
kdf: KdfType,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey> {
let key: Uint8Array = null;
if (kdf == null || kdf === 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) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue;
}
if (kdfConfig.memory == null) {
kdfConfig.memory = ARGON2_MEMORY.defaultValue;
}
if (kdfConfig.parallelism == null) {
kdfConfig.parallelism = ARGON2_PARALLELISM.defaultValue;
}
const saltHash = await this.cryptoFunctionService.hash(salt, "sha256");
key = await this.cryptoFunctionService.argon2(
password,
saltHash,
kdfConfig.iterations,
kdfConfig.memory * 1024, // convert to KiB from MiB
kdfConfig.parallelism,
);
} else {
throw new Error("Unknown Kdf.");
}
return new SymmetricCryptoKey(key);
}
}

View File

@@ -1,10 +1,10 @@
import { any, mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
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 { StateService } from "../../../platform/abstractions/state.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -24,7 +24,7 @@ import { SendService } from "./send.service";
describe("SendService", () => {
const cryptoService = mock<CryptoService>();
const i18nService = mock<I18nService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const keyGenerationService = mock<KeyGenerationService>();
const encryptService = mock<EncryptService>();
let sendService: SendService;
@@ -50,7 +50,7 @@ describe("SendService", () => {
.calledWith(any())
.mockResolvedValue([sendView("1", "Test Send")]);
sendService = new SendService(cryptoService, i18nService, cryptoFunctionService, stateService);
sendService = new SendService(cryptoService, i18nService, keyGenerationService, stateService);
});
afterEach(() => {

View File

@@ -1,9 +1,10 @@
import { BehaviorSubject, Observable, concatMap, distinctUntilChanged, map } from "rxjs";
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { StateService } from "../../../platform/abstractions/state.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";
@@ -21,6 +22,9 @@ import { SEND_KDF_ITERATIONS } from "../send-kdf";
import { InternalSendService as InternalSendServiceAbstraction } from "./send.service.abstraction";
export class SendService implements InternalSendServiceAbstraction {
readonly sendKeySalt = "bitwarden-send";
readonly sendKeyPurpose = "send";
protected _sends: BehaviorSubject<Send[]> = new BehaviorSubject([]);
protected _sendViews: BehaviorSubject<SendView[]> = new BehaviorSubject([]);
@@ -30,7 +34,7 @@ export class SendService implements InternalSendServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private cryptoFunctionService: CryptoFunctionService,
private keyGenerationService: KeyGenerationService,
private stateService: StateService,
) {
this.stateService.activeAccountUnlocked$
@@ -72,17 +76,22 @@ export class SendService implements InternalSendServiceAbstraction {
send.hideEmail = model.hideEmail;
send.maxAccessCount = model.maxAccessCount;
if (model.key == null) {
model.key = await this.cryptoFunctionService.aesGenerateKey(128);
model.cryptoKey = await this.cryptoService.makeSendKey(model.key);
const key = await this.keyGenerationService.createKeyWithPurpose(
128,
this.sendKeyPurpose,
this.sendKeySalt,
);
model.key = key.material;
model.cryptoKey = key.derivedKey;
}
if (password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(
const passwordKey = await this.keyGenerationService.deriveKeyFromPassword(
password,
model.key,
"sha256",
SEND_KDF_ITERATIONS,
KdfType.PBKDF2_SHA256,
{ iterations: SEND_KDF_ITERATIONS },
);
send.password = Utils.fromBufferToB64(passwordHash);
send.password = passwordKey.keyB64;
}
send.key = await this.cryptoService.encrypt(model.key, key);
send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey);