diff --git a/apps/browser/src/key-management/browser-key.service.ts b/apps/browser/src/key-management/browser-key.service.ts index 1fa3e111fed..3d25b03d1b8 100644 --- a/apps/browser/src/key-management/browser-key.service.ts +++ b/apps/browser/src/key-management/browser-key.service.ts @@ -70,7 +70,7 @@ export class BrowserKeyService extends DefaultKeyService { protected override async getKeyFromStorage( keySuffix: KeySuffixOptions, userId?: UserId, - ): Promise { + ): Promise { if (keySuffix === KeySuffixOptions.Biometric) { const biometricsResult = await this.biometricsService.authenticateBiometric(); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 45191a48eb0..bdba17ee5f2 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -17,7 +17,7 @@ export declare namespace passwords { export declare namespace biometrics { export function prompt(hwnd: Buffer, message: string): Promise export function available(): Promise - export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise + export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string | null): Promise export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise /** * Derives key material from biometric data. Returns a string encoded with a diff --git a/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts b/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts index 48b41881bd2..cebb284929b 100644 --- a/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts +++ b/apps/desktop/src/key-management/biometrics/biometric.renderer-ipc.listener.ts @@ -22,7 +22,7 @@ export class BiometricsRendererIPCListener { serviceName += message.keySuffix; } - let val: string | boolean = null; + let val: string | boolean | null = null; if (!message.action) { return val; diff --git a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts index 8962e7f3ecf..49db86dc21c 100644 --- a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts +++ b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts @@ -73,7 +73,7 @@ export default class BiometricUnixMain implements OsBiometricService { return null; } else { const encValue = new EncString(value); - this.setIv(encValue.iv); + this.setIv(encValue.iv ?? null); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); const storedValue = await biometrics.getBiometricSecret( service, @@ -132,7 +132,7 @@ export default class BiometricUnixMain implements OsBiometricService { // Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey // when we want to force a re-derive of the key material. - private setIv(iv: string) { + private setIv(iv: string | null) { this._iv = iv; this._osKeyHalf = null; } @@ -140,8 +140,8 @@ export default class BiometricUnixMain implements OsBiometricService { private async getStorageDetails({ clientKeyHalfB64, }: { - clientKeyHalfB64: string; - }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> { + clientKeyHalfB64: string | undefined; + }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string | null }> { if (this._osKeyHalf == null) { const keyMaterial = await biometrics.deriveKeyMaterial(this._iv); // osKeyHalf is based on the iv and in contrast to windows is not locked behind user verefication! diff --git a/apps/desktop/src/key-management/biometrics/biometric.windows.main.ts b/apps/desktop/src/key-management/biometrics/biometric.windows.main.ts index abda9bf9484..2623c5af4b2 100644 --- a/apps/desktop/src/key-management/biometrics/biometric.windows.main.ts +++ b/apps/desktop/src/key-management/biometrics/biometric.windows.main.ts @@ -52,7 +52,7 @@ export default class BiometricWindowsMain implements OsBiometricService { return value; } else { const encValue = new EncString(value); - this.setIv(encValue.iv); + this.setIv(encValue.iv ?? null); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64, }); @@ -102,8 +102,8 @@ export default class BiometricWindowsMain implements OsBiometricService { private async getStorageDetails({ clientKeyHalfB64, }: { - clientKeyHalfB64: string; - }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> { + clientKeyHalfB64: string | undefined; + }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string | null }> { if (this._osKeyHalf == null) { // Prompts Windows Hello const keyMaterial = await biometrics.deriveKeyMaterial(this._iv); @@ -122,7 +122,7 @@ export default class BiometricWindowsMain implements OsBiometricService { // Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey // when we want to force a re-derive of the key material. - private setIv(iv: string) { + private setIv(iv: string | null) { this._iv = iv; this._osKeyHalf = null; } @@ -141,9 +141,9 @@ export default class BiometricWindowsMain implements OsBiometricService { encryptedValue: EncString, service: string, storageKey: string, - clientKeyPartB64: string, + clientKeyPartB64: string | undefined, ) { - if (encryptedValue.iv == null || encryptedValue == null) { + if (encryptedValue == null || encryptedValue.iv == null) { return; } @@ -175,7 +175,7 @@ export default class BiometricWindowsMain implements OsBiometricService { storageKey, }: { value: SymmetricCryptoKey; - clientKeyPartB64: string; + clientKeyPartB64: string | undefined; service: string; storageKey: string; }): Promise { @@ -206,7 +206,7 @@ export default class BiometricWindowsMain implements OsBiometricService { /** Derives a witness key from a symmetric key being stored for biometric protection */ private witnessKeyMaterial( symmetricKey: SymmetricCryptoKey, - clientKeyPartB64: string, + clientKeyPartB64: string | undefined, ): biometrics.KeyMaterial { const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64; return { diff --git a/apps/desktop/src/key-management/biometrics/biometrics.service.ts b/apps/desktop/src/key-management/biometrics/biometrics.service.ts index e7e0773ad16..aab0835ec9a 100644 --- a/apps/desktop/src/key-management/biometrics/biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/biometrics.service.ts @@ -9,7 +9,7 @@ import { WindowMain } from "../../main/window.main"; import { DesktopBiometricsService, OsBiometricService } from "./desktop.biometrics.service"; export class BiometricsService extends DesktopBiometricsService { - private platformSpecificService: OsBiometricService; + private platformSpecificService: OsBiometricService | undefined; private clientKeyHalves = new Map(); constructor( @@ -65,18 +65,30 @@ export class BiometricsService extends DesktopBiometricsService { } async supportsBiometric() { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } return await this.platformSpecificService.osSupportsBiometric(); } async biometricsNeedsSetup() { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } return await this.platformSpecificService.osBiometricsNeedsSetup(); } async biometricsSupportsAutoSetup() { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } return await this.platformSpecificService.osBiometricsCanAutoSetup(); } async biometricsSetup() { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } await this.platformSpecificService.osBiometricsSetup(); } @@ -96,12 +108,16 @@ export class BiometricsService extends DesktopBiometricsService { } async authenticateBiometric(): Promise { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } + let result = false; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.interruptProcessReload( () => { - return this.platformSpecificService.authenticateBiometric(); + return this.platformSpecificService!.authenticateBiometric(); }, (response) => { result = response; @@ -112,14 +128,22 @@ export class BiometricsService extends DesktopBiometricsService { } async isBiometricUnlockAvailable(): Promise { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } + return await this.platformSpecificService.osSupportsBiometric(); } async getBiometricKey(service: string, storageKey: string): Promise { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } + return await this.interruptProcessReload(async () => { await this.enforceClientKeyHalf(service, storageKey); - return await this.platformSpecificService.getBiometricKey( + return await this.platformSpecificService!.getBiometricKey( service, storageKey, this.getClientKeyHalf(service, storageKey), @@ -128,6 +152,10 @@ export class BiometricsService extends DesktopBiometricsService { } async setBiometricKey(service: string, storageKey: string, value: string): Promise { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } + await this.enforceClientKeyHalf(service, storageKey); return await this.platformSpecificService.setBiometricKey( @@ -156,6 +184,10 @@ export class BiometricsService extends DesktopBiometricsService { } async deleteBiometricKey(service: string, storageKey: string): Promise { + if (this.platformSpecificService === undefined) { + throw new Error("Biometric service not loaded"); + } + this.clientKeyHalves.delete(this.clientKeyHalfKey(service, storageKey)); return await this.platformSpecificService.deleteBiometricKey(service, storageKey); } @@ -163,15 +195,15 @@ export class BiometricsService extends DesktopBiometricsService { private async interruptProcessReload( callback: () => Promise, restartReloadCallback: (arg: T) => boolean = () => false, - ): Promise { + ): Promise { this.messagingService.send("cancelProcessReload"); let restartReload = false; - let response: T; + let response: T | null = null; try { response = await callback(); restartReload ||= restartReloadCallback(response); } catch (error) { - if (error.message === "Biometric authentication failed") { + if (error instanceof Error && error.message === "Biometric authentication failed") { restartReload = false; } else { restartReload = true; diff --git a/apps/web/src/app/auth/emergency-access/index.ts b/apps/web/src/app/auth/emergency-access/index.ts index 3452873710a..d5b0fc6f522 100644 --- a/apps/web/src/app/auth/emergency-access/index.ts +++ b/apps/web/src/app/auth/emergency-access/index.ts @@ -1,2 +1,3 @@ export * from "./emergency-access.module"; export * from "./services"; +export * from "./request/emergency-access-update.request"; diff --git a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts index 81b7d361579..3581304e686 100644 --- a/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/key-management/key-rotation/request/update-key.request.ts @@ -1,5 +1,6 @@ import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; +import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request"; @@ -7,9 +8,12 @@ import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/ import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; export class UpdateKeyRequest { - masterPasswordHash: string; - key: string; - privateKey: string; + constructor( + readonly masterPasswordHash: EncryptedString, + readonly key: string, + readonly privateKey: EncryptedString, + ) {} + ciphers: CipherWithIdRequest[] = []; folders: FolderWithIdRequest[] = []; sends: SendWithIdRequest[] = []; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index 41215d012aa..f5d926ab85f 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -22,9 +22,8 @@ import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/fold import { KeyService } from "@bitwarden/key-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; -import { WebauthnLoginAdminService } from "../core"; -import { EmergencyAccessService } from "../emergency-access"; -import { EmergencyAccessWithIdRequest } from "../emergency-access/request/emergency-access-update.request"; +import { WebauthnLoginAdminService } from "../../auth/core"; +import { EmergencyAccessService, EmergencyAccessWithIdRequest } from "../../auth/emergency-access"; import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; import { UserKeyRotationService } from "./user-key-rotation.service"; diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 6c2f15a02a6..f4bbd910133 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -80,15 +80,8 @@ export class UserKeyRotationService { throw new Error("User key could not be created"); } - // Create new request - const request = new UpdateKeyRequest(); - - // Add new user key - request.key = newEncUserKey.encryptedString; - // Add master key hash const masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey); - request.masterPasswordHash = masterPasswordHash; // Get original user key // Note: We distribute the legacy key, but not all domains actually use it. If any of those @@ -99,7 +92,14 @@ export class UserKeyRotationService { this.logService.info("[Userkey rotation] Is legacy user: " + isMasterKey); // Add re-encrypted data - request.privateKey = await this.encryptPrivateKey(newUserKey, user.id); + const privateKey = (await this.encryptPrivateKey(newUserKey, user.id))!; + + // Create new request + const request = new UpdateKeyRequest( + newEncUserKey.encryptedString!, + masterPasswordHash, + privateKey, + ); const rotatedCiphers = await this.cipherService.getRotatedData( originalUserKey, @@ -177,6 +177,6 @@ export class UserKeyRotationService { if (!privateKey) { throw new Error("No private key found for user key rotation"); } - return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString; + return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString!; } } diff --git a/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts index 68ef95fef6f..7f88187518f 100644 --- a/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts @@ -48,6 +48,9 @@ export class MigrateFromLegacyEncryptionComponent { } const activeUser = await firstValueFrom(this.accountService.activeAccount$); + if (activeUser === null) { + throw new Error("No active user."); + } const hasUserKey = await this.keyService.hasUserKey(activeUser.id); if (hasUserKey) { @@ -56,6 +59,9 @@ export class MigrateFromLegacyEncryptionComponent { } const masterPassword = this.formGroup.value.masterPassword; + if (!masterPassword) { + throw new Error("Master password cannot be empty."); + } try { await this.syncService.fullSync(false, true); @@ -71,7 +77,10 @@ export class MigrateFromLegacyEncryptionComponent { this.messagingService.send("logout"); } catch (e) { // If the error is due to missing folders, we can delete all folders and try again - if (e.message === "All existing folders must be included in the rotation.") { + if ( + e instanceof Error && + e.message === "All existing folders must be included in the rotation." + ) { const deleteFolders = await this.dialogService.openSimpleDialog({ type: "warning", title: { key: "encryptionKeyUpdateCannotProceed" }, diff --git a/libs/auth/src/common/abstractions/pin.service.abstraction.ts b/libs/auth/src/common/abstractions/pin.service.abstraction.ts index 00ccf934f61..f2aa25b1dac 100644 --- a/libs/auth/src/common/abstractions/pin.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/pin.service.abstraction.ts @@ -45,7 +45,7 @@ export abstract class PinServiceAbstraction { /** * Clears the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey. */ - abstract clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise; + abstract clearPinKeyEncryptedUserKeyEphemeral(userId?: UserId): Promise; /** * Creates a pinKeyEncryptedUserKey from the provided PIN and UserKey. @@ -99,7 +99,7 @@ export abstract class PinServiceAbstraction { /** * Clears the old MasterKey, encrypted by the PinKey. */ - abstract clearOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise; + abstract clearOldPinKeyEncryptedMasterKey: (userId?: UserId) => Promise; /** * Makes a PinKey from the provided PIN. diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 2a01802fa57..e0be037a4a5 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -168,7 +168,7 @@ export class PinService implements PinServiceAbstraction { ); } - async clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise { + async clearPinKeyEncryptedUserKeyEphemeral(userId?: UserId): Promise { this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyEphemeral."); await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, null, userId); @@ -249,7 +249,7 @@ export class PinService implements PinServiceAbstraction { ); } - async clearOldPinKeyEncryptedMasterKey(userId: UserId): Promise { + async clearOldPinKeyEncryptedMasterKey(userId?: UserId): Promise { this.validateUserId(userId, "Cannot clear oldPinKeyEncryptedMasterKey."); await this.stateProvider.setUserState(OLD_PIN_KEY_ENCRYPTED_MASTER_KEY, null, userId); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 619bab8f034..28800609a89 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -28,7 +28,7 @@ export abstract class StateService { /** * Sets the user's auto key */ - setUserKeyAutoUnlock: (value: string, options?: StorageOptions) => Promise; + setUserKeyAutoUnlock: (value: string | null, options?: StorageOptions) => Promise; /** * Gets the user's biometric key */ @@ -55,7 +55,7 @@ export abstract class StateService { /** * @deprecated For migration purposes only, use setUserKeyAuto instead */ - setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise; + setCryptoMasterKeyAuto: (value: string | null, options?: StorageOptions) => Promise; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/misc/convert-values.ts b/libs/common/src/platform/misc/convert-values.ts index 7a1087ec360..00d7c24e22b 100644 --- a/libs/common/src/platform/misc/convert-values.ts +++ b/libs/common/src/platform/misc/convert-values.ts @@ -6,7 +6,7 @@ import { ObservableInput, OperatorFunction, map } from "rxjs"; */ export function convertValues( project: (key: TKey, value: TInput) => ObservableInput, -): OperatorFunction, Record>> { +): OperatorFunction | null, Record>> { return map((inputRecord) => { if (inputRecord == null) { return null; diff --git a/libs/common/src/platform/state/implementations/default-state.provider.ts b/libs/common/src/platform/state/implementations/default-state.provider.ts index 22aed80e8af..6691599c634 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.ts @@ -52,7 +52,7 @@ export class DefaultStateProvider implements StateProvider { async setUserState( userKeyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, ): Promise<[UserId, T]> { if (userId) { diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index 44736500afc..0023b717b24 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -60,7 +60,7 @@ export abstract class StateProvider { */ abstract setUserState( keyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, ): Promise<[UserId, T]>; @@ -68,10 +68,13 @@ export abstract class StateProvider { abstract getActive(userKeyDefinition: UserKeyDefinition): ActiveUserState; /** @see{@link SingleUserStateProvider.get} */ - abstract getUser(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState; + abstract getUser( + userId: UserId, + userKeyDefinition: UserKeyDefinition, + ): SingleUserState; /** @see{@link GlobalStateProvider.get} */ - abstract getGlobal(keyDefinition: KeyDefinition): GlobalState; + abstract getGlobal(keyDefinition: KeyDefinition): GlobalState; abstract getDerived( parentState$: Observable, deriveDefinition: DeriveDefinition, diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts index 44bc8732544..984f9740953 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -37,7 +37,7 @@ export interface ActiveUserState extends UserState { * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ readonly update: ( - configureState: (state: T, dependencies: TCombine) => T, + configureState: (state: T | null, dependencies: TCombine) => T | null, options?: StateUpdateOptions, ) => Promise<[UserId, T]>; } @@ -56,7 +56,7 @@ export interface SingleUserState extends UserState { * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ readonly update: ( - configureState: (state: T, dependencies: TCombine) => T, + configureState: (state: T | null, dependencies: TCombine) => T | null, options?: StateUpdateOptions, - ) => Promise; + ) => Promise; } diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index 55ffea9db79..4215ca3fe71 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -1,15 +1,14 @@ import { Observable } from "rxjs"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; - -import { ProfileOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-organization.response"; -import { ProfileProviderOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-provider-organization.response"; -import { ProfileProviderResponse } from "../../../common/src/admin-console/models/response/profile-provider.response"; -import { KdfConfig } from "../../../common/src/auth/models/domain/kdf-config"; -import { KeySuffixOptions, HashPurpose } from "../../../common/src/platform/enums"; -import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "../../../common/src/platform/models/domain/symmetric-crypto-key"; -import { OrganizationId, UserId } from "../../../common/src/types/guid"; +import { ProfileOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-organization.response"; +import { ProfileProviderOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-provider-organization.response"; +import { ProfileProviderResponse } from "@bitwarden/common/admin-console/models/response/profile-provider.response"; +import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, @@ -18,7 +17,7 @@ import { CipherKey, UserPrivateKey, UserPublicKey, -} from "../../../common/src/types/key"; +} from "@bitwarden/common/types/key"; export class UserPrivateKeyDecryptionFailedError extends Error { constructor() { @@ -38,7 +37,7 @@ export type CipherDecryptionKeys = { /** * A users decrypted organization keys. */ - orgKeys: Record; + orgKeys: Record | null; }; export abstract class KeyService { @@ -47,7 +46,8 @@ export abstract class KeyService { * is in a locked or logged out state. * @param userId The user id of the user to get the {@see UserKey} for. */ - abstract userKey$(userId: UserId): Observable; + abstract userKey$(userId: UserId): Observable; + /** * Returns the an observable key for the given user id. * @@ -55,6 +55,7 @@ export abstract class KeyService { * @param userId The desired user */ abstract getInMemoryUserKeyFor$(userId: UserId): Observable; + /** * Sets the provided user key and stores * any other necessary versions (such as auto, biometrics, @@ -64,7 +65,8 @@ export abstract class KeyService { * @param key The user key to set * @param userId The desired user */ - abstract setUserKey(key: UserKey, userId?: string): Promise; + abstract setUserKey(key: UserKey | null, userId?: string): Promise; + /** * Sets the provided user keys and stores any other necessary versions * (such as auto, biometrics, or pin). @@ -79,17 +81,20 @@ export abstract class KeyService { * @param userId The desired user */ abstract setUserKeys(userKey: UserKey, encPrivateKey: string, userId: UserId): Promise; + /** * Gets the user key from memory and sets it again, * kicking off a refresh of any additional keys * (such as auto, biometrics, or pin) */ abstract refreshAdditionalKeys(): Promise; + /** * Observable value that returns whether or not the currently active user has ever had auser key, * i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states. */ abstract everHadUserKey$: Observable; + /** * Retrieves the user key * @param userId The desired user @@ -121,13 +126,17 @@ export abstract class KeyService { * @param userId The desired user */ abstract getUserKeyWithLegacySupport(userId: UserId): Promise; + /** * Retrieves the user key from storage * @param keySuffix The desired version of the user's key to retrieve * @param userId The desired user * @returns The user key */ - abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise; + abstract getUserKeyFromStorage( + keySuffix: KeySuffixOptions, + userId?: string, + ): Promise; /** * Determines whether the user key is available for the given user. @@ -135,41 +144,48 @@ export abstract class KeyService { * @returns True if the user key is available */ abstract hasUserKey(userId?: UserId): Promise; + /** * Determines whether the user key is available for the given user in memory. * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. * @returns True if the user key is available */ abstract hasUserKeyInMemory(userId?: string): Promise; + /** * @param keySuffix The desired version of the user's key to check * @param userId The desired user * @returns True if the provided version of the user key is stored */ abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise; + /** * Generates a new user key * @param masterKey The user's master key * @returns A new user key and the master key protected version of it */ abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>; + /** * Clears the user's stored version of the user key * @param keySuffix The desired version of the key to clear * @param userId The desired user */ abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise; + /** * Stores the master key encrypted user key * @param userKeyMasterKey The master key encrypted user key to set * @param userId The desired user */ - abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise; + abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise; + /** * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user */ abstract getOrDeriveMasterKey(password: string, userId?: string): Promise; + /** * Generates a master key from the provided password * @param password The user's master password @@ -178,6 +194,7 @@ export abstract class KeyService { * @returns A master key derived from the provided password */ abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise; + /** * Encrypts the existing (or provided) user key with the * provided master key @@ -189,6 +206,7 @@ export abstract class KeyService { masterKey: MasterKey, userKey?: UserKey, ): Promise<[UserKey, EncString]>; + /** * Creates a master password hash from the user's master password. Can * be used for local authentication or for server authentication depending @@ -203,6 +221,7 @@ export abstract class KeyService { key: MasterKey, hashPurpose?: HashPurpose, ): Promise; + /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. @@ -212,6 +231,7 @@ export abstract class KeyService { * key hash or the server key hash */ abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise; + /** * Stores the encrypted organization keys and clears any decrypted * organization keys currently in memory @@ -224,6 +244,7 @@ export abstract class KeyService { providerOrgs: ProfileProviderOrganizationResponse[], userId: UserId, ): Promise; + /** * Retrieves a stream of the active users organization keys, * will NOT emit any value if there is no active user. @@ -231,6 +252,7 @@ export abstract class KeyService { * @deprecated Use {@link orgKeys$} with a required {@link UserId} instead. */ abstract activeUserOrgKeys$: Observable>; + /** * Returns the organization's symmetric key * @deprecated Use the observable userOrgKeys$ and `map` to the desired {@link OrgKey} instead @@ -238,6 +260,7 @@ export abstract class KeyService { * @returns The organization's symmetric key */ abstract getOrgKey(orgId: string): Promise; + /** * Uses the org key to derive a new symmetric key for encrypting data * @param orgKey The organization's symmetric key @@ -252,17 +275,20 @@ export abstract class KeyService { * @param userId The user id of the user for which to store the keys for. */ abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise; + /** * @param providerId The desired provider * @returns The provider's symmetric key */ - abstract getProviderKey(providerId: string): Promise; + abstract getProviderKey(providerId: string): Promise; + /** * Creates a new organization key and encrypts it with the user's public key. * This method can also return Provider keys for creating new Provider users. * @returns The new encrypted org key and the decrypted key itself */ abstract makeOrgKey(): Promise<[EncString, T]>; + /** * Sets the user's encrypted private key in storage and * clears the decrypted private key from memory @@ -270,6 +296,7 @@ export abstract class KeyService { * @param encPrivateKey An encrypted private key */ abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise; + /** * Returns the private key from memory. If not available, decrypts it * from storage and stores it in memory @@ -279,7 +306,7 @@ export abstract class KeyService { * * @deprecated Use {@link userPrivateKey$} instead. */ - abstract getPrivateKey(): Promise; + abstract getPrivateKey(): Promise; /** * Gets an observable stream of the given users decrypted private key, will emit null if the user @@ -288,7 +315,7 @@ export abstract class KeyService { * * @param userId The user id of the user to get the data for. */ - abstract userPrivateKey$(userId: UserId): Observable; + abstract userPrivateKey$(userId: UserId): Observable; /** * Gets an observable stream of the given users encrypted private key, will emit null if the user @@ -299,7 +326,7 @@ export abstract class KeyService { * @deprecated Temporary function to allow the SDK to be initialized after the login process, it * will be removed when auth has been migrated to the SDK. */ - abstract userEncryptedPrivateKey$(userId: UserId): Observable; + abstract userEncryptedPrivateKey$(userId: UserId): Observable; /** * Gets an observable stream of the given users decrypted private key with legacy support, @@ -308,7 +335,7 @@ export abstract class KeyService { * * @param userId The user id of the user to get the data for. */ - abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable; + abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable; /** * Generates a fingerprint phrase for the user based on their public key @@ -317,6 +344,7 @@ export abstract class KeyService { * @returns The user's fingerprint phrase */ abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise; + /** * Generates a new keypair * @param key A key to encrypt the private key with. If not provided, @@ -325,6 +353,7 @@ export abstract class KeyService { * @throws If the provided key is a null-ish value. */ abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>; + /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, @@ -332,17 +361,21 @@ export abstract class KeyService { * @param userId The desired user */ abstract clearPinKeys(userId?: string): Promise; + /** * @param keyMaterial The key material to derive the send key from * @returns A new send key */ abstract makeSendKey(keyMaterial: Uint8Array): Promise; + /** * Clears all of the user's keys from storage * @param userId The user's Id */ abstract clearKeys(userId?: string): Promise; + abstract randomNumber(min: number, max: number): Promise; + /** * Generates a new cipher key * @returns A new cipher key @@ -361,6 +394,7 @@ export abstract class KeyService { publicKey: string; privateKey: EncString; }>; + /** * Previously, the master key was used for any additional key like the biometrics or pin key. * We have switched to using the user key for these purposes. This method is for clearing the state @@ -404,7 +438,7 @@ export abstract class KeyService { */ abstract encryptedOrgKeys$( userId: UserId, - ): Observable>; + ): Observable | null>; /** * Gets an observable stream of the users public key. If the user is does not have @@ -414,7 +448,7 @@ export abstract class KeyService { * * @throws If an invalid user id is passed in. */ - abstract userPublicKey$(userId: UserId): Observable; + abstract userPublicKey$(userId: UserId): Observable; /** * Validates that a userkey is correct for a given user diff --git a/libs/key-management/src/biometrics/biometric-state.service.spec.ts b/libs/key-management/src/biometrics/biometric-state.service.spec.ts index 2f11537127b..7aff750941c 100644 --- a/libs/key-management/src/biometrics/biometric-state.service.spec.ts +++ b/libs/key-management/src/biometrics/biometric-state.service.spec.ts @@ -1,15 +1,14 @@ import { firstValueFrom } from "rxjs"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { makeEncString, trackEmissions } from "../../../common/spec"; +import { makeEncString, trackEmissions } from "@bitwarden/common/spec"; import { FakeAccountService, mockAccountServiceWith, -} from "../../../common/spec/fake-account-service"; -import { FakeGlobalState, FakeSingleUserState } from "../../../common/spec/fake-state"; -import { FakeStateProvider } from "../../../common/spec/fake-state-provider"; +} from "@bitwarden/common/spec/fake-account-service"; +import { FakeGlobalState, FakeSingleUserState } from "@bitwarden/common/spec/fake-state"; +import { FakeStateProvider } from "@bitwarden/common/spec/fake-state-provider"; +import { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service"; import { diff --git a/libs/key-management/src/biometrics/biometric-state.service.ts b/libs/key-management/src/biometrics/biometric-state.service.ts index e8153007390..ed055c29e66 100644 --- a/libs/key-management/src/biometrics/biometric-state.service.ts +++ b/libs/key-management/src/biometrics/biometric-state.service.ts @@ -1,8 +1,8 @@ import { Observable, firstValueFrom, map, combineLatest } from "rxjs"; -import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; -import { ActiveUserState, GlobalState, StateProvider } from "../../../common/src/platform/state"; -import { UserId } from "../../../common/src/types/guid"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { ActiveUserState, GlobalState, StateProvider } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; import { BIOMETRIC_UNLOCK_ENABLED, @@ -25,7 +25,7 @@ export abstract class BiometricStateService { * * Tracks the currently active user */ - abstract encryptedClientKeyHalf$: Observable; + abstract encryptedClientKeyHalf$: Observable; /** * whether or not a password is required on first unlock after opening the application * @@ -62,42 +62,54 @@ export abstract class BiometricStateService { * @param value whether or not a password is required on first unlock after opening the application */ abstract setRequirePasswordOnStart(value: boolean): Promise; + /** * Updates the biometric unlock enabled state for the currently active user. * @param enabled whether or not to store a biometric key to unlock the vault */ abstract setBiometricUnlockEnabled(enabled: boolean): Promise; + /** * Gets the biometric unlock enabled state for the given user. * @param userId user Id to check */ abstract getBiometricUnlockEnabled(userId: UserId): Promise; + abstract setEncryptedClientKeyHalf(encryptedKeyHalf: EncString, userId?: UserId): Promise; - abstract getEncryptedClientKeyHalf(userId: UserId): Promise; + + abstract getEncryptedClientKeyHalf(userId: UserId): Promise; + abstract getRequirePasswordOnStart(userId: UserId): Promise; + abstract removeEncryptedClientKeyHalf(userId: UserId): Promise; + /** * Updates the active user's state to reflect that they've been warned about requiring password on start. */ abstract setDismissedRequirePasswordOnStartCallout(): Promise; + /** * Updates the active user's state to reflect that they've cancelled the biometric prompt. */ abstract setUserPromptCancelled(): Promise; + /** * Resets the given user's state to reflect that they haven't cancelled the biometric prompt. * @param userId the user to reset the prompt cancelled state for. If not provided, the currently active user will be used. */ abstract resetUserPromptCancelled(userId?: UserId): Promise; + /** * Resets all user's state to reflect that they haven't cancelled the biometric prompt. */ abstract resetAllPromptCancelled(): Promise; + /** * Updates the currently active user's setting for auto prompting for biometrics on application start and lock * @param prompt Whether or not to prompt for biometrics on application start. */ abstract setPromptAutomatically(prompt: boolean): Promise; + /** * Updates whether or not IPC has been validated by the user this session * @param validated the value to save @@ -110,13 +122,13 @@ export abstract class BiometricStateService { export class DefaultBiometricStateService implements BiometricStateService { private biometricUnlockEnabledState: ActiveUserState; private requirePasswordOnStartState: ActiveUserState; - private encryptedClientKeyHalfState: ActiveUserState; + private encryptedClientKeyHalfState: ActiveUserState; private dismissedRequirePasswordOnStartCalloutState: ActiveUserState; - private promptCancelledState: GlobalState>; + private promptCancelledState: GlobalState | null>; private promptAutomaticallyState: ActiveUserState; - private fingerprintValidatedState: GlobalState; + private fingerprintValidatedState: GlobalState; biometricUnlockEnabled$: Observable; - encryptedClientKeyHalf$: Observable; + encryptedClientKeyHalf$: Observable; requirePasswordOnStart$: Observable; dismissedRequirePasswordOnStartCallout$: Observable; promptCancelled$: Observable; @@ -149,7 +161,7 @@ export class DefaultBiometricStateService implements BiometricStateService { this.promptCancelledState.state$, ]).pipe( map(([userId, record]) => { - return record?.[userId] ?? false; + return userId ? (record?.[userId] ?? false) : false; }), ); this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY); @@ -170,7 +182,7 @@ export class DefaultBiometricStateService implements BiometricStateService { } async setRequirePasswordOnStart(value: boolean): Promise { - let currentActiveId: UserId; + let currentActiveId: UserId | undefined = undefined; await this.requirePasswordOnStartState.update( (_, [userId]) => { currentActiveId = userId; @@ -180,7 +192,7 @@ export class DefaultBiometricStateService implements BiometricStateService { combineLatestWith: this.requirePasswordOnStartState.combinedState$, }, ); - if (!value) { + if (!value && currentActiveId) { await this.removeEncryptedClientKeyHalf(currentActiveId); } } @@ -204,7 +216,7 @@ export class DefaultBiometricStateService implements BiometricStateService { )); } - async getEncryptedClientKeyHalf(userId: UserId): Promise { + async getEncryptedClientKeyHalf(userId: UserId): Promise { return await firstValueFrom( this.stateProvider .getUser(userId, ENCRYPTED_CLIENT_KEY_HALF) @@ -226,7 +238,9 @@ export class DefaultBiometricStateService implements BiometricStateService { async resetUserPromptCancelled(userId: UserId): Promise { await this.stateProvider.getGlobal(PROMPT_CANCELLED).update( (data, activeUserId) => { - delete data[userId ?? activeUserId]; + if (data) { + delete data[userId ?? activeUserId]; + } return data; }, { @@ -239,14 +253,16 @@ export class DefaultBiometricStateService implements BiometricStateService { async setUserPromptCancelled(): Promise { await this.promptCancelledState.update( (record, userId) => { - record ??= {}; - record[userId] = true; + if (userId) { + record ??= {}; + record[userId] = true; + } return record; }, { combineLatestWith: this.stateProvider.activeUserId$, shouldUpdate: (_, userId) => { - if (userId == null) { + if (!userId) { throw new Error( "Cannot update biometric prompt cancelled state without an active user", ); @@ -271,7 +287,7 @@ export class DefaultBiometricStateService implements BiometricStateService { } function encryptedClientKeyHalfToEncString( - encryptedKeyHalf: EncryptedString | undefined, -): EncString { - return encryptedKeyHalf == null ? null : new EncString(encryptedKeyHalf); + encryptedKeyHalf: EncryptedString | null | undefined, +): EncString | null { + return !encryptedKeyHalf ? null : new EncString(encryptedKeyHalf); } diff --git a/libs/key-management/src/biometrics/biometric.state.ts b/libs/key-management/src/biometrics/biometric.state.ts index f88bd1da581..d1349429e44 100644 --- a/libs/key-management/src/biometrics/biometric.state.ts +++ b/libs/key-management/src/biometrics/biometric.state.ts @@ -1,10 +1,10 @@ -import { EncryptedString } from "../../../common/src/platform/models/domain/enc-string"; +import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyDefinition, BIOMETRIC_SETTINGS_DISK, UserKeyDefinition, -} from "../../../common/src/platform/state"; -import { UserId } from "../../../common/src/types/guid"; +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; /** * Indicates whether the user elected to store a biometric key to unlock their vault. diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 263779f59b3..8a7fdffa544 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -1,42 +1,48 @@ import { mock } from "jest-mock-extended"; import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs"; -import { PinServiceAbstraction } from "../../auth/src/common/abstractions"; +import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { awaitAsync, makeEncString, makeStaticByteArray, makeSymmetricCryptoKey, -} from "../../common/spec"; -import { FakeAccountService, mockAccountServiceWith } from "../../common/spec/fake-account-service"; -import { FakeActiveUserState, FakeSingleUserState } from "../../common/spec/fake-state"; -import { FakeStateProvider } from "../../common/spec/fake-state-provider"; -import { EncryptedOrganizationKeyData } from "../../common/src/admin-console/models/data/encrypted-organization-key.data"; -import { KdfConfigService } from "../../common/src/auth/abstractions/kdf-config.service"; -import { FakeMasterPasswordService } from "../../common/src/auth/services/master-password/fake-master-password.service"; -import { CryptoFunctionService } from "../../common/src/platform/abstractions/crypto-function.service"; -import { EncryptService } from "../../common/src/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "../../common/src/platform/abstractions/key-generation.service"; -import { LogService } from "../../common/src/platform/abstractions/log.service"; -import { PlatformUtilsService } from "../../common/src/platform/abstractions/platform-utils.service"; -import { StateService } from "../../common/src/platform/abstractions/state.service"; -import { Encrypted } from "../../common/src/platform/interfaces/encrypted"; -import { Utils } from "../../common/src/platform/misc/utils"; -import { EncString, EncryptedString } from "../../common/src/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "../../common/src/platform/models/domain/symmetric-crypto-key"; -import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "../../common/src/platform/services/key-state/org-keys.state"; -import { USER_ENCRYPTED_PROVIDER_KEYS } from "../../common/src/platform/services/key-state/provider-keys.state"; +} from "@bitwarden/common/spec"; +import { + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/spec/fake-account-service"; +import { FakeActiveUserState, FakeSingleUserState } from "@bitwarden/common/spec/fake-state"; +import { FakeStateProvider } from "@bitwarden/common/spec/fake-state-provider"; +import { EncryptedOrganizationKeyData } from "@bitwarden/common/src/admin-console/models/data/encrypted-organization-key.data"; +import { KdfConfigService } from "@bitwarden/common/src/auth/abstractions/kdf-config.service"; +import { FakeMasterPasswordService } from "@bitwarden/common/src/auth/services/master-password/fake-master-password.service"; +import { CryptoFunctionService } from "@bitwarden/common/src/platform/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/src/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/src/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/src/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/src/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/src/platform/abstractions/state.service"; +import { Encrypted } from "@bitwarden/common/src/platform/interfaces/encrypted"; +import { Utils } from "@bitwarden/common/src/platform/misc/utils"; +import { + EncString, + EncryptedString, +} from "@bitwarden/common/src/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/src/platform/models/domain/symmetric-crypto-key"; +import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "@bitwarden/common/src/platform/services/key-state/org-keys.state"; +import { USER_ENCRYPTED_PROVIDER_KEYS } from "@bitwarden/common/src/platform/services/key-state/provider-keys.state"; import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY, USER_KEY, -} from "../../common/src/platform/services/key-state/user-key.state"; -import { UserKeyDefinition } from "../../common/src/platform/state"; -import { VAULT_TIMEOUT } from "../../common/src/services/vault-timeout/vault-timeout-settings.state"; -import { CsprngArray } from "../../common/src/types/csprng"; -import { OrganizationId, UserId } from "../../common/src/types/guid"; -import { UserKey, MasterKey } from "../../common/src/types/key"; -import { VaultTimeoutStringType } from "../../common/src/types/vault-timeout.type"; +} from "@bitwarden/common/src/platform/services/key-state/user-key.state"; +import { UserKeyDefinition } from "@bitwarden/common/src/platform/state"; +import { VAULT_TIMEOUT } from "@bitwarden/common/src/services/vault-timeout/vault-timeout-settings.state"; +import { CsprngArray } from "@bitwarden/common/src/types/csprng"; +import { OrganizationId, UserId } from "@bitwarden/common/src/types/guid"; +import { UserKey, MasterKey } from "@bitwarden/common/src/types/key"; +import { VaultTimeoutStringType } from "@bitwarden/common/src/types/vault-timeout.type"; import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service"; import { DefaultKeyService } from "./key.service"; diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index b12db176cec..1b6707e87ce 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -1,58 +1,58 @@ import * as bigInt from "big-integer"; import { - NEVER, - Observable, combineLatest, firstValueFrom, forkJoin, map, + NEVER, + Observable, of, switchMap, } from "rxjs"; -import { PinServiceAbstraction } from "../../auth/src/common/abstractions"; -import { EncryptedOrganizationKeyData } from "../../common/src/admin-console/models/data/encrypted-organization-key.data"; -import { BaseEncryptedOrganizationKey } from "../../common/src/admin-console/models/domain/encrypted-organization-key"; -import { ProfileOrganizationResponse } from "../../common/src/admin-console/models/response/profile-organization.response"; -import { ProfileProviderOrganizationResponse } from "../../common/src/admin-console/models/response/profile-provider-organization.response"; -import { ProfileProviderResponse } from "../../common/src/admin-console/models/response/profile-provider.response"; -import { AccountService } from "../../common/src/auth/abstractions/account.service"; -import { KdfConfigService } from "../../common/src/auth/abstractions/kdf-config.service"; -import { InternalMasterPasswordServiceAbstraction } from "../../common/src/auth/abstractions/master-password.service.abstraction"; -import { KdfConfig } from "../../common/src/auth/models/domain/kdf-config"; -import { CryptoFunctionService } from "../../common/src/platform/abstractions/crypto-function.service"; -import { EncryptService } from "../../common/src/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "../../common/src/platform/abstractions/key-generation.service"; -import { LogService } from "../../common/src/platform/abstractions/log.service"; -import { PlatformUtilsService } from "../../common/src/platform/abstractions/platform-utils.service"; -import { StateService } from "../../common/src/platform/abstractions/state.service"; -import { KeySuffixOptions, HashPurpose } from "../../common/src/platform/enums"; -import { convertValues } from "../../common/src/platform/misc/convert-values"; -import { Utils } from "../../common/src/platform/misc/utils"; -import { EFFLongWordList } from "../../common/src/platform/misc/wordlist"; -import { EncString, EncryptedString } from "../../common/src/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "../../common/src/platform/models/domain/symmetric-crypto-key"; -import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "../../common/src/platform/services/key-state/org-keys.state"; -import { USER_ENCRYPTED_PROVIDER_KEYS } from "../../common/src/platform/services/key-state/provider-keys.state"; +import { PinServiceAbstraction } from "@bitwarden/auth/common"; +import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; +import { BaseEncryptedOrganizationKey } from "@bitwarden/common/admin-console/models/domain/encrypted-organization-key"; +import { ProfileOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-organization.response"; +import { ProfileProviderOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-provider-organization.response"; +import { ProfileProviderResponse } from "@bitwarden/common/admin-console/models/response/profile-provider.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { convertValues } from "@bitwarden/common/platform/misc/convert-values"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "@bitwarden/common/platform/services/key-state/org-keys.state"; +import { USER_ENCRYPTED_PROVIDER_KEYS } from "@bitwarden/common/platform/services/key-state/provider-keys.state"; import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY, USER_KEY, -} from "../../common/src/platform/services/key-state/user-key.state"; -import { ActiveUserState, StateProvider } from "../../common/src/platform/state"; -import { VAULT_TIMEOUT } from "../../common/src/services/vault-timeout/vault-timeout-settings.state"; -import { CsprngArray } from "../../common/src/types/csprng"; -import { OrganizationId, ProviderId, UserId } from "../../common/src/types/guid"; +} from "@bitwarden/common/platform/services/key-state/user-key.state"; +import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; +import { VAULT_TIMEOUT } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.state"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { - OrgKey, - UserKey, - MasterKey, - ProviderKey, CipherKey, + MasterKey, + OrgKey, + ProviderKey, + UserKey, UserPrivateKey, UserPublicKey, -} from "../../common/src/types/key"; -import { VaultTimeoutStringType } from "../../common/src/types/vault-timeout.type"; +} from "@bitwarden/common/types/key"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherDecryptionKeys, @@ -85,15 +85,15 @@ export class DefaultKeyService implements KeyServiceAbstraction { this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false)); this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe( - switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)), - ); + switchMap((userId) => (userId ? this.orgKeys$(userId) : NEVER)), + ) as Observable>; } - async setUserKey(key: UserKey, userId: UserId): Promise { - if (key == null) { + async setUserKey(key: UserKey | null, userId?: UserId): Promise { + if (!key) { throw new Error("No key provided. Lock the user to clear the key"); } - if (userId == null) { + if (!userId) { throw new Error("No userId provided."); } @@ -144,12 +144,15 @@ export class DefaultKeyService implements KeyServiceAbstraction { } async getUserKey(userId?: UserId): Promise { - const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - return userKey; + return await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise { userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); return await this.validateUserKey(masterKey as unknown as UserKey, userId); @@ -159,6 +162,10 @@ export class DefaultKeyService implements KeyServiceAbstraction { async getUserKeyWithLegacySupport(userId?: UserId): Promise { userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -170,8 +177,15 @@ export class DefaultKeyService implements KeyServiceAbstraction { return masterKey as unknown as UserKey; } - async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise { + async getUserKeyFromStorage( + keySuffix: KeySuffixOptions, + userId?: UserId, + ): Promise { userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + const userKey = await this.getKeyFromStorage(keySuffix, userId); if (userKey) { if (!(await this.validateUserKey(userKey, userId))) { @@ -180,6 +194,8 @@ export class DefaultKeyService implements KeyServiceAbstraction { } return userKey; } + + return null; } async hasUserKey(userId?: UserId): Promise { @@ -206,6 +222,10 @@ export class DefaultKeyService implements KeyServiceAbstraction { async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> { if (!masterKey) { const userId = await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); } if (masterKey == null) { @@ -249,8 +269,12 @@ export class DefaultKeyService implements KeyServiceAbstraction { } } - async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise { + async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise { userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + await this.masterPasswordService.setMasterKeyEncryptedUserKey( new EncString(userKeyMasterKey), userId, @@ -263,6 +287,10 @@ export class DefaultKeyService implements KeyServiceAbstraction { combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe( map(([activeAccount, accounts]) => { userId ??= activeAccount?.id; + if (!userId) { + throw new Error("User ID is required"); + } + return [userId, accounts[userId]?.email]; }), ), @@ -306,6 +334,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { ): Promise { if (!key) { const userId = await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); } @@ -321,6 +352,10 @@ export class DefaultKeyService implements KeyServiceAbstraction { // TODO: move to MasterPasswordService async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise { const userId = await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + const storedPasswordHash = await firstValueFrom( this.masterPasswordService.masterKeyHash$(userId), ); @@ -378,10 +413,13 @@ export class DefaultKeyService implements KeyServiceAbstraction { async getOrgKey(orgId: OrganizationId): Promise { const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - if (activeUserId == null) { + if (!activeUserId) { throw new Error("A user must be active to retrieve an org key"); } const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId)); + if (!orgKeys) { + throw new Error("No org keys found"); + } return orgKeys[orgId]; } @@ -417,13 +455,20 @@ export class DefaultKeyService implements KeyServiceAbstraction { } // TODO: Deprecate in favor of observable - async getProviderKey(providerId: ProviderId): Promise { + async getProviderKey(providerId: ProviderId): Promise { if (providerId == null) { return null; } const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + if (!activeUserId) { + throw new Error("A user must be active to retrieve a provider key"); + } + const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId)); + if (!providerKeys) { + throw new Error("No provider keys found"); + } return providerKeys[providerId] ?? null; } @@ -440,7 +485,15 @@ export class DefaultKeyService implements KeyServiceAbstraction { async makeOrgKey(userId?: UserId): Promise<[EncString, T]> { const shareKey = await this.keyGenerationService.createKey(512); userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + if (!userId) { + throw new Error("User ID is required."); + } + const publicKey = await firstValueFrom(this.userPublicKey$(userId)); + if (!publicKey) { + throw new Error("No public key found"); + } + const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey); return [encShareKey, shareKey as T]; } @@ -455,10 +508,10 @@ export class DefaultKeyService implements KeyServiceAbstraction { .update(() => encPrivateKey); } - async getPrivateKey(): Promise { + async getPrivateKey(): Promise { const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - if (activeUserId == null) { + if (!activeUserId) { throw new Error("User must be active while attempting to retrieve private key."); } @@ -466,15 +519,23 @@ export class DefaultKeyService implements KeyServiceAbstraction { } // TODO: Make public key required - async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise { - if (publicKey == null) { + async getFingerprint( + fingerprintMaterial: string, + publicKey?: Uint8Array | null, + ): Promise { + if (!publicKey) { const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + if (!activeUserId) { + throw new Error("A user must be active to retrieve a fingerprint"); + } + publicKey = await firstValueFrom(this.userPublicKey$(activeUserId)); } - if (publicKey === null) { + if (!publicKey) { throw new Error("No public key available."); } + const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256"); const userFingerprint = await this.cryptoFunctionService.hkdfExpand( keyFingerprint, @@ -500,8 +561,8 @@ export class DefaultKeyService implements KeyServiceAbstraction { * Clears the user's key pair * @param userId The desired user */ - private async clearKeyPair(userId: UserId): Promise { - if (userId == null) { + private async clearKeyPair(userId?: UserId): Promise { + if (!userId) { // nothing to do return; } @@ -633,7 +694,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { }> { const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - if (activeUserId == null) { + if (!activeUserId) { throw new Error("Cannot initilize an account if one is not active."); } @@ -650,7 +711,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { await this.setUserKey(userKey, activeUserId); await this.stateProvider .getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY) - .update(() => privateKey.encryptedString); + .update(() => privateKey.encryptedString!); return { userKey, @@ -721,6 +782,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { break; } case KeySuffixOptions.Pin: { + if (!userId) { + throw new Error("User ID is required."); + } const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId); shouldStoreKey = !!userKeyEncryptedPin; break; @@ -732,7 +796,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { protected async getKeyFromStorage( keySuffix: KeySuffixOptions, userId?: UserId, - ): Promise { + ): Promise { if (keySuffix === KeySuffixOptions.Auto) { const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId }); if (userKey) { @@ -771,7 +835,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { encryptionKey: SymmetricCryptoKey, newSymKey: Uint8Array, ): Promise<[T, EncString]> { - let protectedSymKey: EncString = null; + let protectedSymKey: EncString; if (encryptionKey.key.byteLength === 32) { const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey); protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey); @@ -796,7 +860,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { } } - userKey$(userId: UserId): Observable { + userKey$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_KEY).state$; } @@ -823,30 +887,34 @@ export class DefaultKeyService implements KeyServiceAbstraction { ); } - userPublicKey$(userId: UserId) { + userPublicKey$(userId: UserId): Observable { return this.userPrivateKey$(userId).pipe( switchMap(async (pk) => await this.derivePublicKey(pk)), ); } - private async derivePublicKey(privateKey: UserPrivateKey) { - if (privateKey == null) { + private async derivePublicKey(privateKey: UserPrivateKey | null) { + if (!privateKey) { return null; } return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; } - userPrivateKey$(userId: UserId): Observable { - return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey)); + userPrivateKey$(userId: UserId): Observable { + return this.userPrivateKeyHelper$(userId, false).pipe( + map((keys) => keys?.userPrivateKey ?? null), + ); } - userEncryptedPrivateKey$(userId: UserId): Observable { + userEncryptedPrivateKey$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$; } - userPrivateKeyWithLegacySupport$(userId: UserId): Observable { - return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey)); + userPrivateKeyWithLegacySupport$(userId: UserId): Observable { + return this.userPrivateKeyHelper$(userId, true).pipe( + map((keys) => keys?.userPrivateKey ?? null), + ); } private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) { @@ -872,8 +940,11 @@ export class DefaultKeyService implements KeyServiceAbstraction { ); } - private async decryptPrivateKey(encryptedPrivateKey: EncryptedString, key: SymmetricCryptoKey) { - if (encryptedPrivateKey == null) { + private async decryptPrivateKey( + encryptedPrivateKey: EncryptedString | null, + key: SymmetricCryptoKey, + ) { + if (!encryptedPrivateKey) { return null; } @@ -903,7 +974,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { private providerKeysHelper$( userId: UserId, userPrivateKey: UserPrivateKey, - ): Observable> { + ): Observable | null> { return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe( // Convert each value in the record to it's own decryption observable convertValues(async (_, value) => { @@ -930,12 +1001,12 @@ export class DefaultKeyService implements KeyServiceAbstraction { } orgKeys$(userId: UserId): Observable | null> { - return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys)); + return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys ?? null)); } encryptedOrgKeys$( userId: UserId, - ): Observable> { + ): Observable | null> { return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$; } @@ -961,16 +1032,24 @@ export class DefaultKeyService implements KeyServiceAbstraction { this.providerKeysHelper$(userId, userPrivateKey), ]).pipe( switchMap(async ([encryptedOrgKeys, providerKeys]) => { + encryptedOrgKeys = encryptedOrgKeys ?? {}; + const result: Record = {}; - for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) { - if (result[orgId] != null) { + for (const orgId of Object.keys(encryptedOrgKeys) as OrganizationId[]) { + if (result[orgId] !== undefined) { continue; } const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]); + if (encrypted === null) { + continue; + } let decrypted: OrgKey; if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) { + if (!providerKeys) { + throw new Error("No provider keys found."); + } decrypted = await encrypted.decrypt(this.encryptService, providerKeys); } else { decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey);