From 19a373d87e21a7edb03faa28b7b154afd1c082a6 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Fri, 23 Feb 2024 08:48:15 -0500 Subject: [PATCH] [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 --- .../device-trust-crypto-service.factory.ts | 6 ++ .../key-connector-service.factory.ts | 12 +-- .../browser/src/background/main.background.ts | 15 ++- .../service-factories/send-service.factory.ts | 8 +- .../crypto-service.factory.ts | 6 ++ .../key-generation-service.factory.ts | 7 +- .../abstract-key-generation.service.ts | 5 - .../services/key-generation.service.ts | 20 ---- ...cal-backed-session-storage.service.spec.ts | 10 +- .../local-backed-session-storage.service.ts | 15 ++- .../src/popup/services/services.module.ts | 5 +- apps/cli/src/bw.ts | 11 +- .../src/app/services/services.module.ts | 2 + .../services/electron-crypto.service.spec.ts | 3 + .../services/electron-crypto.service.ts | 3 + .../service-accounts/access/access.service.ts | 20 ++-- .../src/services/jslib-services.module.ts | 13 ++- ...ice-trust-crypto.service.implementation.ts | 8 +- .../device-trust-crypto.service.spec.ts | 14 ++- .../auth/services/key-connector.service.ts | 8 +- .../abstractions/key-generation.service.ts | 59 ++++++++++ .../platform/services/crypto.service.spec.ts | 3 + .../src/platform/services/crypto.service.ts | 79 ++++---------- .../services/key-generation.service.spec.ts | 102 ++++++++++++++++++ .../services/key-generation.service.ts | 85 +++++++++++++++ .../tools/send/services/send.service.spec.ts | 6 +- .../src/tools/send/services/send.service.ts | 25 +++-- 27 files changed, 401 insertions(+), 149 deletions(-) delete mode 100644 apps/browser/src/platform/services/abstractions/abstract-key-generation.service.ts delete mode 100644 apps/browser/src/platform/services/key-generation.service.ts create mode 100644 libs/common/src/platform/abstractions/key-generation.service.ts create mode 100644 libs/common/src/platform/services/key-generation.service.spec.ts create mode 100644 libs/common/src/platform/services/key-generation.service.ts diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts index 8458c40b9b..6b8d3c09e3 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts @@ -30,6 +30,10 @@ import { I18nServiceInitOptions, i18nServiceFactory, } from "../../../platform/background/service-factories/i18n-service.factory"; +import { + KeyGenerationServiceInitOptions, + keyGenerationServiceFactory, +} from "../../../platform/background/service-factories/key-generation-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -42,6 +46,7 @@ import { type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & + KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & CryptoServiceInitOptions & EncryptServiceInitOptions & @@ -61,6 +66,7 @@ export function deviceTrustCryptoServiceFactory( opts, async () => new DeviceTrustCryptoService( + await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts index 625a14e30e..5fd1866c83 100644 --- a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts @@ -9,10 +9,6 @@ import { apiServiceFactory, ApiServiceInitOptions, } from "../../../platform/background/service-factories/api-service.factory"; -import { - CryptoFunctionServiceInitOptions, - cryptoFunctionServiceFactory, -} from "../../../platform/background/service-factories/crypto-function-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -22,6 +18,10 @@ import { CachedServices, factory, } from "../../../platform/background/service-factories/factory-options"; +import { + KeyGenerationServiceInitOptions, + keyGenerationServiceFactory, +} from "../../../platform/background/service-factories/key-generation-service.factory"; import { logServiceFactory, LogServiceInitOptions, @@ -46,7 +46,7 @@ export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & TokenServiceInitOptions & LogServiceInitOptions & OrganizationServiceInitOptions & - CryptoFunctionServiceInitOptions; + KeyGenerationServiceInitOptions; export function keyConnectorServiceFactory( cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices, @@ -64,7 +64,7 @@ export function keyConnectorServiceFactory( await tokenServiceFactory(cache, opts), await logServiceFactory(cache, opts), await organizationServiceFactory(cache, opts), - await cryptoFunctionServiceFactory(cache, opts), + await keyGenerationServiceFactory(cache, opts), opts.keyConnectorServiceOptions.logoutCallback, ), ); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index bdc9d21d91..3df24d40fe 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -54,6 +54,7 @@ import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/pla import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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 as LogServiceAbstraction } 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"; @@ -72,6 +73,7 @@ import { ContainerService } from "@bitwarden/common/platform/services/container. import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -180,7 +182,6 @@ import BrowserMessagingPrivateModeBackgroundService from "../platform/services/b import BrowserMessagingService from "../platform/services/browser-messaging.service"; import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserStateService } from "../platform/services/browser-state.service"; -import { KeyGenerationService } from "../platform/services/key-generation.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; @@ -207,6 +208,7 @@ export default class MainBackground { i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; + keyGenerationService: KeyGenerationServiceAbstraction; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; tokenService: TokenServiceAbstraction; @@ -326,6 +328,7 @@ export default class MainBackground { ? new BrowserMessagingPrivateModeBackgroundService() : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); + this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.storageService = new BrowserLocalStorageService(); this.secureStorageService = new BrowserLocalStorageService(); @@ -333,14 +336,14 @@ export default class MainBackground { BrowserApi.manifestVersion === 3 ? new LocalBackedSessionStorageService( new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - new KeyGenerationService(this.cryptoFunctionService), + this.keyGenerationService, ) : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.manifestVersion === 3 ? new LocalBackedSessionStorageService( new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - new KeyGenerationService(this.cryptoFunctionService), + this.keyGenerationService, ) : new BackgroundMemoryStorageService(); this.globalStateProvider = new DefaultGlobalStateProvider( @@ -426,6 +429,7 @@ export default class MainBackground { ); this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService); this.cryptoService = new BrowserCryptoService( + this.keyGenerationService, this.cryptoFunctionService, this.encryptService, this.platformUtilsService, @@ -478,7 +482,7 @@ export default class MainBackground { this.tokenService, this.logService, this.organizationService, - this.cryptoFunctionService, + this.keyGenerationService, logoutCallback, ); @@ -506,6 +510,7 @@ export default class MainBackground { this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, this.encryptService, @@ -636,7 +641,7 @@ export default class MainBackground { this.sendService = new BrowserSendService( this.cryptoService, this.i18nService, - this.cryptoFunctionService, + this.keyGenerationService, this.stateService, ); this.sendApiService = new SendApiService( diff --git a/apps/browser/src/background/service-factories/send-service.factory.ts b/apps/browser/src/background/service-factories/send-service.factory.ts index bb5ef1fa37..bca46b4703 100644 --- a/apps/browser/src/background/service-factories/send-service.factory.ts +++ b/apps/browser/src/background/service-factories/send-service.factory.ts @@ -1,6 +1,5 @@ import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { cryptoFunctionServiceFactory } from "../../platform/background/service-factories/crypto-function-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -14,6 +13,10 @@ import { i18nServiceFactory, I18nServiceInitOptions, } from "../../platform/background/service-factories/i18n-service.factory"; +import { + KeyGenerationServiceInitOptions, + keyGenerationServiceFactory, +} from "../../platform/background/service-factories/key-generation-service.factory"; import { stateServiceFactory, StateServiceInitOptions, @@ -25,6 +28,7 @@ type SendServiceFactoryOptions = FactoryOptions; export type SendServiceInitOptions = SendServiceFactoryOptions & CryptoServiceInitOptions & I18nServiceInitOptions & + KeyGenerationServiceInitOptions & StateServiceInitOptions; export function sendServiceFactory( @@ -39,7 +43,7 @@ export function sendServiceFactory( new BrowserSendService( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), - await cryptoFunctionServiceFactory(cache, opts), + await keyGenerationServiceFactory(cache, opts), await stateServiceFactory(cache, opts), ), ); diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index 1b09f4a9c3..765530beb6 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -20,6 +20,10 @@ import { } from "./crypto-function-service.factory"; import { encryptServiceFactory, EncryptServiceInitOptions } from "./encrypt-service.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; +import { + KeyGenerationServiceInitOptions, + keyGenerationServiceFactory, +} from "./key-generation-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -29,6 +33,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider type CryptoServiceFactoryOptions = FactoryOptions; export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & + KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & EncryptServiceInitOptions & PlatformUtilsServiceInitOptions & @@ -47,6 +52,7 @@ export function cryptoServiceFactory( opts, async () => new BrowserCryptoService( + await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/background/service-factories/key-generation-service.factory.ts b/apps/browser/src/platform/background/service-factories/key-generation-service.factory.ts index d4f6d52738..accba0023a 100644 --- a/apps/browser/src/platform/background/service-factories/key-generation-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/key-generation-service.factory.ts @@ -1,4 +1,5 @@ -import { KeyGenerationService } from "../../services/key-generation.service"; +import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { cryptoFunctionServiceFactory, @@ -12,9 +13,9 @@ export type KeyGenerationServiceInitOptions = KeyGenerationServiceFactoryOptions CryptoFunctionServiceInitOptions; export function keyGenerationServiceFactory( - cache: { keyGenerationService?: KeyGenerationService } & CachedServices, + cache: { keyGenerationService?: KeyGenerationServiceAbstraction } & CachedServices, opts: KeyGenerationServiceInitOptions, -): Promise { +): Promise { return factory( cache, "keyGenerationService", diff --git a/apps/browser/src/platform/services/abstractions/abstract-key-generation.service.ts b/apps/browser/src/platform/services/abstractions/abstract-key-generation.service.ts deleted file mode 100644 index 5c2751dcef..0000000000 --- a/apps/browser/src/platform/services/abstractions/abstract-key-generation.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; - -export interface AbstractKeyGenerationService { - makeEphemeralKey(numBytes?: number): Promise; -} diff --git a/apps/browser/src/platform/services/key-generation.service.ts b/apps/browser/src/platform/services/key-generation.service.ts deleted file mode 100644 index 6b6f552c71..0000000000 --- a/apps/browser/src/platform/services/key-generation.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; - -import { AbstractKeyGenerationService } from "./abstractions/abstract-key-generation.service"; - -export class KeyGenerationService implements AbstractKeyGenerationService { - constructor(private cryptoFunctionService: CryptoFunctionService) {} - - async makeEphemeralKey(numBytes = 16): Promise { - const keyMaterial = await this.cryptoFunctionService.randomBytes(numBytes); - const key = await this.cryptoFunctionService.hkdf( - keyMaterial, - "bitwarden-ephemeral", - "ephemeral", - 64, - "sha256", - ); - return new SymmetricCryptoKey(key); - } -} diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index b6d6b99c01..fff9f2c28f 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -1,13 +1,13 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import BrowserLocalStorageService from "./browser-local-storage.service"; import BrowserMemoryStorageService from "./browser-memory-storage.service"; -import { KeyGenerationService } from "./key-generation.service"; import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; describe("Browser Session Storage Service", () => { @@ -206,7 +206,11 @@ describe("Browser Session Storage Service", () => { describe("new key creation", () => { beforeEach(() => { jest.spyOn(sessionStorage, "get").mockResolvedValue(null); - keyGenerationService.makeEphemeralKey.mockResolvedValue(key); + keyGenerationService.createKeyWithPurpose.mockResolvedValue({ + salt: "salt", + material: null, + derivedKey: key, + }); jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); }); @@ -214,7 +218,7 @@ describe("Browser Session Storage Service", () => { const result = await sut.getSessionEncKey(); expect(result).toStrictEqual(key); - expect(keyGenerationService.makeEphemeralKey).toBeCalledTimes(1); + expect(keyGenerationService.createKeyWithPurpose).toBeCalledTimes(1); }); it("should store a symmetric crypto key if it makes one", async () => { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index d17a338f7f..b2823ffe4b 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -2,6 +2,7 @@ import { Subject } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { AbstractMemoryStorageService, StorageUpdate, @@ -13,7 +14,6 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { devFlag } from "../decorators/dev-flag.decorator"; import { devFlagEnabled } from "../flags"; -import { AbstractKeyGenerationService } from "./abstractions/abstract-key-generation.service"; import BrowserLocalStorageService from "./browser-local-storage.service"; import BrowserMemoryStorageService from "./browser-memory-storage.service"; @@ -31,7 +31,7 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi constructor( private encryptService: EncryptService, - private keyGenerationService: AbstractKeyGenerationService, + private keyGenerationService: KeyGenerationService, ) { super(); this.updates$ = this.updatesSubject.asObservable(); @@ -138,10 +138,17 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi async getSessionEncKey(): Promise { let storedKey = await this.sessionStorage.get(keys.encKey); if (storedKey == null || Object.keys(storedKey).length == 0) { - storedKey = await this.keyGenerationService.makeEphemeralKey(); + const generatedKey = await this.keyGenerationService.createKeyWithPurpose( + 128, + "ephemeral", + "bitwarden-ephemeral", + ); + storedKey = generatedKey.derivedKey; await this.setSessionEncKey(storedKey); + return storedKey; + } else { + return SymmetricCryptoKey.fromJSON(storedKey); } - return SymmetricCryptoKey.fromJSON(storedKey); } async setSessionEncKey(input: SymmetricCryptoKey): Promise { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index c76e34e44d..55457d1c70 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -53,6 +53,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileUploadService } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService, LogService as LogServiceAbstraction, @@ -358,13 +359,13 @@ function getBgService(service: keyof MainBackground) { useFactory: ( cryptoService: CryptoService, i18nService: I18nServiceAbstraction, - cryptoFunctionService: CryptoFunctionService, + keyGenerationService: KeyGenerationService, stateServiceAbstraction: StateServiceAbstraction, ) => { return new BrowserSendService( cryptoService, i18nService, - cryptoFunctionService, + keyGenerationService, stateServiceAbstraction, ); }, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index d1c46e9e0b..e9acb7a5af 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -38,6 +38,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/services/user-ve import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ClientType } from "@bitwarden/common/enums"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; +import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Account } from "@bitwarden/common/platform/models/domain/account"; @@ -50,6 +51,7 @@ import { CryptoService } from "@bitwarden/common/platform/services/crypto.servic import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/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 { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -164,6 +166,7 @@ export class Main { individualExportService: IndividualVaultExportServiceAbstraction; organizationExportService: OrganizationVaultExportServiceAbstraction; searchService: SearchService; + keyGenerationService: KeyGenerationServiceAbstraction; cryptoFunctionService: NodeCryptoFunctionService; encryptService: EncryptServiceImplementation; authService: AuthService; @@ -296,7 +299,10 @@ export class Main { migrationRunner, ); + this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + this.cryptoService = new CryptoService( + this.keyGenerationService, this.cryptoFunctionService, this.encryptService, this.platformUtilsService, @@ -337,7 +343,7 @@ export class Main { this.sendService = new SendService( this.cryptoService, this.i18nService, - this.cryptoFunctionService, + this.keyGenerationService, this.stateService, ); @@ -383,7 +389,7 @@ export class Main { this.tokenService, this.logService, this.organizationService, - this.cryptoFunctionService, + this.keyGenerationService, async (expired: boolean) => await this.logout(), ); @@ -399,6 +405,7 @@ export class Main { this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, this.encryptService, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index d1dca936b5..06ac9e1deb 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -25,6 +25,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.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, LogService as LogServiceAbstraction, @@ -183,6 +184,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ + KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, PlatformUtilsServiceAbstraction, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 8dbc9294f4..58ea5c412e 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; @@ -23,6 +24,7 @@ import { ElectronStateService } from "./electron-state.service.abstraction"; describe("electronCryptoService", () => { let sut: ElectronCryptoService; + const keyGenerationService = mock(); const cryptoFunctionService = mock(); const encryptService = mock(); const platformUtilService = mock(); @@ -39,6 +41,7 @@ describe("electronCryptoService", () => { stateProvider = new FakeStateProvider(accountService); sut = new ElectronCryptoService( + keyGenerationService, cryptoFunctionService, encryptService, platformUtilService, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index ab9974c955..184be5e1fb 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,6 +1,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; @@ -18,6 +19,7 @@ import { ElectronStateService } from "./electron-state.service.abstraction"; export class ElectronCryptoService extends CryptoService { constructor( + keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, platformUtilsService: PlatformUtilsService, @@ -28,6 +30,7 @@ export class ElectronCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + keyGenerationService, cryptoFunctionService, encryptService, platformUtilsService, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts index 93fb09307b..c0e13961c0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts @@ -3,9 +3,9 @@ import { Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -28,7 +28,7 @@ export class AccessService { constructor( private cryptoService: CryptoService, private apiService: ApiService, - private cryptoFunctionService: CryptoFunctionService, + private keyGenerationService: KeyGenerationService, private encryptService: EncryptService, ) {} @@ -53,19 +53,15 @@ export class AccessService { serviceAccountId: string, accessTokenView: AccessTokenView, ): Promise { - const keyMaterial = await this.cryptoFunctionService.aesGenerateKey(128); - const key = await this.cryptoFunctionService.hkdf( - keyMaterial, - "bitwarden-accesstoken", + const key = await this.keyGenerationService.createKeyWithPurpose( + 128, "sm-access-token", - 64, - "sha256", + "bitwarden-accesstoken", ); - const encryptionKey = new SymmetricCryptoKey(key); const request = await this.createAccessTokenRequest( organizationId, - encryptionKey, + key.derivedKey, accessTokenView, ); const r = await this.apiService.send( @@ -77,8 +73,8 @@ export class AccessService { ); const result = new AccessTokenCreationResponse(r); this._accessToken.next(null); - const b64Key = Utils.fromBufferToB64(keyMaterial); - return `${this._accessTokenVersion}.${result.id}.${result.clientSecret}:${b64Key}`; + const keyB64 = Utils.fromBufferToB64(key.material); + return `${this._accessTokenVersion}.${result.id}.${result.clientSecret}:${keyB64}`; } async revokeAccessTokens(serviceAccountId: string, accessTokenIds: string[]): Promise { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b833a3b929..6f7716227d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -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, diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts index a5abac5e82..9aa558ba53 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts @@ -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 { // 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( diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts index 0fbb954fb3..8c0a62b125 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts @@ -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(); const cryptoFunctionService = mock(); const cryptoService = mock(); const encryptService = mock(); @@ -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); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index 654caca845..cded13a74b 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -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, ) {} @@ -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, diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts new file mode 100644 index 0000000000..a015182f89 --- /dev/null +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -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; + /** + * 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; + /** + * 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; +} diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index fd4e5b92c7..a8fa48f36f 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -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(); const cryptoFunctionService = mock(); const encryptService = mock(); const platformUtilService = mock(); @@ -38,6 +40,7 @@ describe("cryptoService", () => { stateProvider = new FakeStateProvider(accountService); cryptoService = new CryptoService( + keyGenerationService, cryptoFunctionService, encryptService, platformUtilService, diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 83faf4f7d3..82dd7ce5c2 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -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; 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 { @@ -294,7 +297,12 @@ export class CryptoService implements CryptoServiceAbstraction { kdf: KdfType, KdfConfig: KdfConfig, ): Promise { - 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 { @@ -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 { @@ -522,10 +530,10 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeOrgKey(): 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 { @@ -588,7 +596,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise { - 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 { - const sendKey = await this.cryptoFunctionService.hkdf( + async makeSendKey(keyMaterial: CsprngArray): Promise { + return await this.keyGenerationService.deriveKeyFromMaterial( keyMaterial, "bitwarden-send", "send", - 64, - "sha256", ); - return new SymmetricCryptoKey(sendKey); } async makeCipherKey(): Promise { - 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 { @@ -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 { - 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. diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts new file mode 100644 index 0000000000..b3e0aa6d4e --- /dev/null +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -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(); + + 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); + }); + }); +}); diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts new file mode 100644 index 0000000000..c592f35e5f --- /dev/null +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -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 { + 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 { + 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 { + 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); + } +} diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 65bc5b5e35..568bd70d52 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -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(); const i18nService = mock(); - const cryptoFunctionService = mock(); + const keyGenerationService = mock(); const encryptService = mock(); 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(() => { diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 0002cad379..528f90c1dc 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -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 = new BehaviorSubject([]); protected _sendViews: BehaviorSubject = 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);