From ac11768843d4fa4d1294d68af57421e427c836df Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 23 Jan 2025 23:00:08 +0000 Subject: [PATCH 1/8] PM-14445: TS strict for Key Management Biometrics --- .../background/nativeMessaging.background.ts | 158 +++++++++++------- .../main-biometrics-ipc.listener.ts | 6 + .../biometrics/main-biometrics.service.ts | 21 ++- .../biometrics/os-biometrics-linux.service.ts | 14 +- .../os-biometrics-windows.service.ts | 20 ++- .../biometric-message-handler.service.ts | 67 +++++--- libs/common/spec/fake-state-provider.ts | 8 +- libs/common/spec/fake-state.ts | 8 +- .../implementations/default-state.provider.ts | 4 +- .../src/platform/state/state.provider.ts | 11 +- libs/common/src/platform/state/user-state.ts | 8 +- .../src/dialog/simple-dialog/types.ts | 2 +- .../biometric-state.service.spec.ts | 31 ++-- .../src/biometrics/biometric-state.service.ts | 68 ++++---- .../src/biometrics/biometric.state.spec.ts | 8 +- .../src/biometrics/biometric.state.ts | 12 +- 16 files changed, 257 insertions(+), 189 deletions(-) diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 21c8f351e8d..08250c938df 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -57,26 +55,29 @@ type ReceiveMessageOuter = { messageId?: number; // Should only have one of these. - message?: EncString; + message?: ReceiveMessage | EncString; sharedSecret?: string; }; type Callback = { - resolver: any; - rejecter: any; + resolver: (value?: unknown) => void; + rejecter: (reason?: any) => void; +}; + +type SecureChannel = { + privateKey: Uint8Array; + publicKey: Uint8Array; + sharedSecret?: SymmetricCryptoKey; + setupResolve: (value?: unknown) => void; }; export class NativeMessagingBackground { connected = false; - private connecting: boolean; - private port: browser.runtime.Port | chrome.runtime.Port; + private connecting: boolean = false; + private port?: browser.runtime.Port | chrome.runtime.Port; + private appId?: string; - private privateKey: Uint8Array = null; - private publicKey: Uint8Array = null; - private secureSetupResolve: any = null; - private sharedSecret: SymmetricCryptoKey; - private appId: string; - private validatingFingerprint: boolean; + private secure?: SecureChannel; private messageId = 0; private callbacks = new Map(); @@ -108,11 +109,13 @@ export class NativeMessagingBackground { async connect() { this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app..."); - this.appId = await this.appIdService.getAppId(); + const appId = await this.appIdService.getAppId(); + this.appId = appId; await this.biometricStateService.setFingerprintValidated(false); return new Promise((resolve, reject) => { - this.port = BrowserApi.connectNative("com.8bit.bitwarden"); + const port = BrowserApi.connectNative("com.8bit.bitwarden"); + this.port = port; this.connecting = true; @@ -131,7 +134,8 @@ export class NativeMessagingBackground { connectedCallback(); } - this.port.onMessage.addListener(async (message: ReceiveMessageOuter) => { + port.onMessage.addListener(async (messageRaw: unknown) => { + const message = messageRaw as ReceiveMessageOuter; switch (message.command) { case "connected": connectedCallback(); @@ -142,7 +146,7 @@ export class NativeMessagingBackground { reject(new Error("startDesktop")); } this.connected = false; - this.port.disconnect(); + port.disconnect(); // reject all for (const callback of this.callbacks.values()) { callback.rejecter("disconnected"); @@ -151,18 +155,31 @@ export class NativeMessagingBackground { break; case "setupEncryption": { // Ignore since it belongs to another device - if (message.appId !== this.appId) { + if (message.appId !== appId) { + return; + } + + if (message.sharedSecret == null) { + this.logService.info( + "[Native Messaging IPC] Unable to create secure channel, no shared secret", + ); + return; + } + if (this.secure == null) { + this.logService.info( + "[Native Messaging IPC] Unable to create secure channel, no secure communication setup", + ); return; } const encrypted = Utils.fromB64ToArray(message.sharedSecret); const decrypted = await this.cryptoFunctionService.rsaDecrypt( encrypted, - this.privateKey, + this.secure.privateKey, HashAlgorithmForEncryption, ); - this.sharedSecret = new SymmetricCryptoKey(decrypted); + this.secure.sharedSecret = new SymmetricCryptoKey(decrypted); this.logService.info("[Native Messaging IPC] Secure channel established"); if ("messageId" in message) { @@ -173,26 +190,27 @@ export class NativeMessagingBackground { this.isConnectedToOutdatedDesktopClient = true; } - this.secureSetupResolve(); + this.secure.setupResolve(); break; } case "invalidateEncryption": // Ignore since it belongs to another device - if (message.appId !== this.appId) { + if (message.appId !== appId) { return; } this.logService.warning( "[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...", ); - this.sharedSecret = null; - this.privateKey = null; + this.secure = undefined; this.connected = false; - if (this.callbacks.has(message.messageId)) { - this.callbacks.get(message.messageId).rejecter({ - message: "invalidateEncryption", - }); + if (message.messageId != null) { + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId)?.rejecter({ + message: "invalidateEncryption", + }); + } } return; case "verifyFingerprint": { @@ -217,21 +235,25 @@ export class NativeMessagingBackground { break; } case "wrongUserId": - if (this.callbacks.has(message.messageId)) { - this.callbacks.get(message.messageId).rejecter({ - message: "wrongUserId", - }); + if (message.messageId) { + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId)?.rejecter({ + message: "wrongUserId", + }); + } } return; default: // Ignore since it belongs to another device - if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) { + if (!this.platformUtilsService.isSafari() && message.appId !== appId) { return; } - // 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.onMessage(message.message); + if (message.message != null) { + // 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.onMessage(message.message); + } } }); @@ -240,16 +262,15 @@ export class NativeMessagingBackground { if (BrowserApi.isWebExtensionsApi) { error = p.error.message; } else { - error = chrome.runtime.lastError.message; + error = chrome.runtime.lastError?.message; } - this.sharedSecret = null; - this.privateKey = null; + this.secure = undefined; this.connected = false; this.logService.error("NativeMessaging port disconnected because of error: " + error); - const reason = error != null ? "desktopIntegrationDisabled" : null; + const reason = error != null ? "desktopIntegrationDisabled" : undefined; reject(new Error(reason)); }); }); @@ -293,13 +314,13 @@ export class NativeMessagingBackground { ); const callback = this.callbacks.get(messageId); this.callbacks.delete(messageId); - callback.rejecter("errorConnecting"); + callback?.rejecter("errorConnecting"); } setTimeout(() => { if (this.callbacks.has(messageId)) { this.logService.info("[Native Messaging IPC] Message timed out and received no response"); - this.callbacks.get(messageId).rejecter({ + this.callbacks.get(messageId)!.rejecter({ message: "timeout", }); this.callbacks.delete(messageId); @@ -320,16 +341,16 @@ export class NativeMessagingBackground { if (this.platformUtilsService.isSafari()) { this.postMessage(message as any); } else { - this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) }); + this.postMessage({ appId: this.appId!, message: await this.encryptMessage(message) }); } } async encryptMessage(message: Message) { - if (this.sharedSecret == null) { + if (this.secure?.sharedSecret == null) { await this.secureCommunication(); } - return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret); + return await this.encryptService.encrypt(JSON.stringify(message), this.secure!.sharedSecret!); } private postMessage(message: OuterMessage, messageId?: number) { @@ -346,7 +367,7 @@ export class NativeMessagingBackground { mac: message.message.mac, }; } - this.port.postMessage(msg); + this.port!.postMessage(msg); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { @@ -354,26 +375,30 @@ export class NativeMessagingBackground { "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.", ); - this.sharedSecret = null; - this.privateKey = null; + this.secure = undefined; this.connected = false; - if (this.callbacks.has(messageId)) { - this.callbacks.get(messageId).rejecter("invalidateEncryption"); + if (messageId != null && this.callbacks.has(messageId)) { + this.callbacks.get(messageId)?.rejecter("invalidateEncryption"); } } } private async onMessage(rawMessage: ReceiveMessage | EncString) { - let message = rawMessage as ReceiveMessage; + let message: ReceiveMessage; if (!this.platformUtilsService.isSafari()) { + if (this.secure?.sharedSecret == null) { + return; + } message = JSON.parse( await this.encryptService.decryptToUtf8( rawMessage as EncString, - this.sharedSecret, + this.secure.sharedSecret, "ipc-desktop-ipc-channel-key", ), ); + } else { + message = rawMessage as ReceiveMessage; } if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { @@ -390,15 +415,17 @@ export class NativeMessagingBackground { this.logService.info( `[Native Messaging IPC] Received legacy message of type ${message.command}`, ); - const messageId = this.callbacks.keys().next().value; - const resolver = this.callbacks.get(messageId); - this.callbacks.delete(messageId); - resolver.resolver(message); + const messageId: number | undefined = this.callbacks.keys().next().value; + if (messageId != null) { + const resolver = this.callbacks.get(messageId); + this.callbacks.delete(messageId); + resolver!.resolver(message); + } return; } if (this.callbacks.has(messageId)) { - this.callbacks.get(messageId).resolver(message); + this.callbacks.get(messageId)!.resolver(message); } else { this.logService.info("[Native Messaging IPC] Received message without a callback", message); } @@ -406,8 +433,6 @@ export class NativeMessagingBackground { private async secureCommunication() { const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); - this.publicKey = publicKey; - this.privateKey = privateKey; const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -419,7 +444,13 @@ export class NativeMessagingBackground { messageId: this.messageId++, }); - return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); + return new Promise((resolve) => { + this.secure = { + publicKey, + privateKey, + setupResolve: resolve, + }; + }); } private async sendUnencrypted(message: Message) { @@ -429,11 +460,14 @@ export class NativeMessagingBackground { message.timestamp = Date.now(); - this.postMessage({ appId: this.appId, message: message }); + this.postMessage({ appId: this.appId!, message: message }); } private async showFingerprintDialog() { - const fingerprint = await this.keyService.getFingerprint(this.appId, this.publicKey); + if (this.secure?.publicKey == null) { + return; + } + const fingerprint = await this.keyService.getFingerprint(this.appId!, this.secure.publicKey); this.messagingService.send("showNativeMessagingFingerprintDialog", { fingerprint: fingerprint, diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts index eebafd8d48b..1605f650e12 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts @@ -32,6 +32,9 @@ export class MainBiometricsIPCListener { case BiometricAction.GetStatusForUser: return await this.biometricService.getBiometricsStatusForUser(message.userId as UserId); case BiometricAction.SetKeyForUser: + if (message.key == null) { + return; + } return await this.biometricService.setBiometricProtectedUnlockKeyForUser( message.userId as UserId, message.key, @@ -41,6 +44,9 @@ export class MainBiometricsIPCListener { message.userId as UserId, ); case BiometricAction.SetClientKeyHalf: + if (message.key == null) { + return; + } return await this.biometricService.setClientKeyHalfForUser( message.userId as UserId, message.key, diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts index d0ba66fdad4..013e69f118c 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -25,10 +25,6 @@ export class MainBiometricsService extends DesktopBiometricsService { private biometricStateService: BiometricStateService, ) { super(); - this.loadOsBiometricService(this.platform); - } - - private loadOsBiometricService(platform: NodeJS.Platform) { if (platform === "win32") { // eslint-disable-next-line const OsBiometricsServiceWindows = require("./os-biometrics-windows.service").default; @@ -117,13 +113,16 @@ export class MainBiometricsService extends DesktopBiometricsService { } async unlockWithBiometricsForUser(userId: UserId): Promise { - return SymmetricCryptoKey.fromString( - await this.osBiometricsService.getBiometricKey( - "Bitwarden_biometric", - `${userId}_user_biometric`, - this.clientKeyHalves.get(userId), - ), - ) as UserKey; + const biometricKey = await this.osBiometricsService.getBiometricKey( + "Bitwarden_biometric", + `${userId}_user_biometric`, + this.clientKeyHalves.get(userId), + ); + if (biometricKey == null) { + return null; + } + + return SymmetricCryptoKey.fromString(biometricKey) as UserKey; } async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise { diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts index 791b4d6f885..fb150f2a653 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { spawn } from "child_process"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -138,23 +136,27 @@ export default class OsBiometricsServiceLinux 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) { - this._iv = iv; + private setIv(iv?: string) { + this._iv = iv ?? null; this._osKeyHalf = null; } private async getStorageDetails({ clientKeyHalfB64, }: { - clientKeyHalfB64: string; + clientKeyHalfB64: string | undefined; }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> { 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! + // osKeyHalf is based on the iv and in contrast to windows is not locked behind user verification! this._osKeyHalf = keyMaterial.keyB64; this._iv = keyMaterial.ivB64; } + if (this._iv == null) { + throw new Error("Initialization Vector is null"); + } + return { key_material: { osKeyPartB64: this._osKeyHalf, diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts index 9643c2b6f15..cd8c94329bc 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -104,7 +102,7 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { private async getStorageDetails({ clientKeyHalfB64, }: { - clientKeyHalfB64: string; + clientKeyHalfB64: string | undefined; }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> { if (this._osKeyHalf == null) { // Prompts Windows Hello @@ -113,6 +111,10 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { this._iv = keyMaterial.ivB64; } + if (this._iv == null) { + throw new Error("Initialization Vector is null"); + } + const result = { key_material: { osKeyPartB64: this._osKeyHalf, @@ -130,8 +132,8 @@ export default class OsBiometricsServiceWindows 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) { - this._iv = iv; + private setIv(iv?: string) { + this._iv = iv ?? null; this._osKeyHalf = null; } @@ -149,9 +151,9 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { encryptedValue: EncString, service: string, storageKey: string, - clientKeyPartB64: string, + clientKeyPartB64: string | undefined, ) { - if (encryptedValue.iv == null || encryptedValue == null) { + if (encryptedValue.iv == null) { return; } @@ -183,7 +185,7 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { storageKey, }: { value: SymmetricCryptoKey; - clientKeyPartB64: string; + clientKeyPartB64: string | undefined; service: string; storageKey: string; }): Promise { @@ -214,7 +216,7 @@ export default class OsBiometricsServiceWindows 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; diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts index 0482434708e..e9e34884d24 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Injectable, NgZone } from "@angular/core"; import { combineLatest, concatMap, firstValueFrom, map } from "rxjs"; @@ -25,8 +23,7 @@ import { } from "@bitwarden/key-management"; import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component"; -import { LegacyMessage } from "../models/native-messaging/legacy-message"; -import { LegacyMessageWrapper } from "../models/native-messaging/legacy-message-wrapper"; +import { LegacyMessage , LegacyMessageWrapper } from "../models/native-messaging"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; const MessageValidTimeout = 10 * 1000; @@ -34,14 +31,14 @@ const HashAlgorithmForAsymmetricEncryption = "sha1"; type ConnectedApp = { publicKey: string; - sessionSecret: string; + sessionSecret: string | null; trusted: boolean; }; const ConnectedAppPrefix = "connectedApp_"; class ConnectedApps { - async get(appId: string): Promise { + async get(appId: string): Promise { if (!(await this.has(appId))) { return null; } @@ -112,6 +109,12 @@ export class BiometricMessageHandlerService { // Request to setup secure encryption if ("command" in rawMessage && rawMessage.command === "setupEncryption") { + if (rawMessage.publicKey == null || rawMessage.userId == null) { + this.logService.warning( + "[Native Messaging IPC] Received invalid setupEncryption message. Ignoring.", + ); + return; + } const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey); // Validate the UserId to ensure we are logged into the same account. @@ -134,16 +137,18 @@ export class BiometricMessageHandlerService { ); } - await this.connectedApps.set(appId, { + const connectedApp = { publicKey: Utils.fromBufferToB64(remotePublicKey), sessionSecret: null, trusted: false, - }); - await this.secureCommunication(remotePublicKey, appId); + } as ConnectedApp; + await this.connectedApps.set(appId, connectedApp); + await this.secureCommunication(connectedApp, remotePublicKey, appId); return; } - if ((await this.connectedApps.get(appId))?.sessionSecret == null) { + const sessionSecret = (await this.connectedApps.get(appId))?.sessionSecret; + if (sessionSecret == null) { this.logService.info( "[Native Messaging IPC] Session secret for secure channel is missing. Invalidating encryption...", ); @@ -157,7 +162,7 @@ export class BiometricMessageHandlerService { const message: LegacyMessage = JSON.parse( await this.encryptService.decryptToUtf8( rawMessage as EncString, - SymmetricCryptoKey.fromString((await this.connectedApps.get(appId)).sessionSecret), + SymmetricCryptoKey.fromString(sessionSecret), ), ); @@ -173,7 +178,10 @@ export class BiometricMessageHandlerService { return; } - if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { + if ( + message.timestamp == null || + Math.abs(message.timestamp - Date.now()) > MessageValidTimeout + ) { this.logService.info("[Native Messaging IPC] Received a too old message. Ignoring."); return; } @@ -277,11 +285,11 @@ export class BiometricMessageHandlerService { return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId); } - const biometricUnlockPromise = + const biometricUnlock = message.userId == null - ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) - : this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId); - if (!(await biometricUnlockPromise)) { + ? await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) + : await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId); + if (!biometricUnlock) { await this.send({ command: "biometricUnlock", response: "not enabled" }, appId); return this.ngZone.run(() => @@ -310,13 +318,13 @@ export class BiometricMessageHandlerService { const currentlyActiveAccountId = ( await firstValueFrom(this.accountService.activeAccount$) - ).id; + )?.id; const isCurrentlyActiveAccountUnlocked = (await this.authService.getAuthStatus(userId)) == AuthenticationStatus.Unlocked; // prevent proc reloading an active account, when it is the same as the browser if (currentlyActiveAccountId != message.userId || !isCurrentlyActiveAccountUnlocked) { - await ipc.platform.reloadProcess(); + ipc.platform.reloadProcess(); } } else { await this.send({ command: "biometricUnlock", response: "canceled" }, appId); @@ -337,9 +345,14 @@ export class BiometricMessageHandlerService { private async send(message: any, appId: string) { message.timestamp = Date.now(); + const sessionSecret = (await this.connectedApps.get(appId))?.sessionSecret; + if (sessionSecret == null) { + throw new Error("Session secret is missing"); + } + const encrypted = await this.encryptService.encrypt( JSON.stringify(message), - SymmetricCryptoKey.fromString((await this.connectedApps.get(appId)).sessionSecret), + SymmetricCryptoKey.fromString(sessionSecret), ); ipc.platform.nativeMessaging.sendMessage({ @@ -349,9 +362,13 @@ export class BiometricMessageHandlerService { }); } - private async secureCommunication(remotePublicKey: Uint8Array, appId: string) { + private async secureCommunication( + connectedApp: ConnectedApp, + remotePublicKey: Uint8Array, + appId: string, + ) { const secret = await this.cryptoFunctionService.randomBytes(64); - const connectedApp = await this.connectedApps.get(appId); + connectedApp.sessionSecret = new SymmetricCryptoKey(secret).keyB64; await this.connectedApps.set(appId, connectedApp); @@ -421,11 +438,15 @@ export class BiometricMessageHandlerService { } } - /** A process reload after a biometric unlock should happen if the userkey that was used for biometric unlock is for a different user than the + /** + * A process reload after a biometric unlock should happen if the userkey that was used for biometric unlock is for a different user than the * currently active account. The userkey for the active account was in memory anyways. Further, if the desktop app is locked, a reload should occur (since the userkey was not already in memory). */ async processReloadWhenRequired(messageUserId: UserId) { - const currentlyActiveAccountId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const currentlyActiveAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (currentlyActiveAccountId == null) { + return; + } const isCurrentlyActiveAccountUnlocked = (await firstValueFrom(this.authService.authStatusFor$(currentlyActiveAccountId))) == AuthenticationStatus.Unlocked; diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index b5105bb24ba..c2e850b7726 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -225,9 +225,9 @@ export class FakeStateProvider implements StateProvider { async setUserState( userKeyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, - ): Promise<[UserId, T]> { + ): Promise<[UserId, T | null]> { await this.mock.setUserState(userKeyDefinition, value, userId); if (userId) { return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)]; @@ -240,11 +240,11 @@ export class FakeStateProvider implements StateProvider { return this.activeUser.get(userKeyDefinition); } - getGlobal(keyDefinition: KeyDefinition): GlobalState { + getGlobal(keyDefinition: KeyDefinition): GlobalState { return this.global.get(keyDefinition); } - getUser(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { + getUser(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { return this.singleUser.get(userId, userKeyDefinition); } diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts index e4b42e357b6..00b12de2eb1 100644 --- a/libs/common/spec/fake-state.ts +++ b/libs/common/spec/fake-state.ts @@ -131,9 +131,9 @@ export class FakeSingleUserState implements SingleUserState { } async update( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options?: StateUpdateOptions, - ): Promise { + ): Promise { options = populateOptionsWithDefault(options); const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout))); const combinedDependencies = @@ -206,9 +206,9 @@ export class FakeActiveUserState implements ActiveUserState { } async update( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options?: StateUpdateOptions, - ): Promise<[UserId, T]> { + ): Promise<[UserId, T | null]> { options = populateOptionsWithDefault(options); const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout))); const combinedDependencies = 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 f86ba11b268..31795767979 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.ts @@ -54,9 +54,9 @@ export class DefaultStateProvider implements StateProvider { async setUserState( userKeyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, - ): Promise<[UserId, T]> { + ): Promise<[UserId, T | null]> { if (userId) { return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)]; } else { diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index 44736500afc..a37726c60a7 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -60,18 +60,21 @@ export abstract class StateProvider { */ abstract setUserState( keyDefinition: UserKeyDefinition, - value: T, + value: T | null, userId?: UserId, - ): Promise<[UserId, T]>; + ): Promise<[UserId, T | null]>; /** @see{@link ActiveUserStateProvider.get} */ 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..5a72189ab32 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -37,9 +37,9 @@ 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]>; + ) => Promise<[UserId, T | null]>; } export interface SingleUserState extends UserState { readonly userId: UserId; @@ -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/components/src/dialog/simple-dialog/types.ts b/libs/components/src/dialog/simple-dialog/types.ts index 4032b499cbe..9724111b4c2 100644 --- a/libs/components/src/dialog/simple-dialog/types.ts +++ b/libs/components/src/dialog/simple-dialog/types.ts @@ -46,7 +46,7 @@ export type SimpleDialogOptions = { * If null is provided, the cancel button will be removed. * * If not localized, pass in a `Translation` */ - cancelButtonText?: string | Translation; + cancelButtonText?: string | Translation | null; /** Whether or not the user can use escape or clicking the backdrop to close the dialog */ disableClose?: boolean; 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..850f461d908 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,18 @@ 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, + FakeStateProvider, + FakeGlobalState, + FakeSingleUserState, +} 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 { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service"; import { @@ -51,7 +54,7 @@ describe("BiometricStateService", () => { it("emits false when the require password on start state is undefined", async () => { const state = stateProvider.activeUser.getFake(REQUIRE_PASSWORD_ON_START); - state.nextState(undefined); + state.nextState(undefined as unknown as boolean); expect(await firstValueFrom(sut.requirePasswordOnStart$)).toBe(false); }); @@ -60,14 +63,14 @@ describe("BiometricStateService", () => { describe("encryptedClientKeyHalf$", () => { it("emits when the encryptedClientKeyHalf state changes", async () => { const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF); - state.nextState(encryptedClientKeyHalf); + state.nextState(encryptedClientKeyHalf as unknown as EncryptedString); expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toEqual(encClientKeyHalf); }); it("emits false when the encryptedClientKeyHalf state is undefined", async () => { const state = stateProvider.activeUser.getFake(ENCRYPTED_CLIENT_KEY_HALF); - state.nextState(undefined); + state.nextState(undefined as unknown as EncryptedString); expect(await firstValueFrom(sut.encryptedClientKeyHalf$)).toBe(null); }); @@ -76,7 +79,7 @@ describe("BiometricStateService", () => { describe("fingerprintValidated$", () => { it("emits when the fingerprint validated state changes", async () => { const state = stateProvider.global.getFake(FINGERPRINT_VALIDATED); - state.stateSubject.next(undefined); + state.stateSubject.next(undefined as unknown as boolean); expect(await firstValueFrom(sut.fingerprintValidated$)).toBe(false); @@ -172,7 +175,7 @@ describe("BiometricStateService", () => { }); it("throws when called with no active user", async () => { - await accountService.switchAccount(null); + await accountService.switchAccount(null as unknown as UserId); await expect(sut.setUserPromptCancelled()).rejects.toThrow( "Cannot update biometric prompt cancelled state without an active user", ); @@ -261,7 +264,7 @@ describe("BiometricStateService", () => { it("emits false when biometricUnlockEnabled state is undefined", async () => { const state = stateProvider.activeUser.getFake(BIOMETRIC_UNLOCK_ENABLED); - state.nextState(undefined); + state.nextState(undefined as unknown as boolean); expect(await firstValueFrom(sut.biometricUnlockEnabled$)).toBe(false); }); @@ -291,7 +294,9 @@ describe("BiometricStateService", () => { }); it("returns false when the state is not set", async () => { - stateProvider.singleUser.getFake(userId, BIOMETRIC_UNLOCK_ENABLED).nextState(undefined); + stateProvider.singleUser + .getFake(userId, BIOMETRIC_UNLOCK_ENABLED) + .nextState(undefined as unknown as boolean); expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false); }); diff --git a/libs/key-management/src/biometrics/biometric-state.service.ts b/libs/key-management/src/biometrics/biometric-state.service.ts index ae6e52ce632..80ae273b416 100644 --- a/libs/key-management/src/biometrics/biometric-state.service.ts +++ b/libs/key-management/src/biometrics/biometric-state.service.ts @@ -1,16 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable, firstValueFrom, map, combineLatest } from "rxjs"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ActiveUserState, GlobalState, StateProvider } from "../../../common/src/platform/state"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -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, @@ -34,7 +26,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 * @@ -71,42 +63,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 @@ -115,7 +119,7 @@ export abstract class BiometricStateService { abstract updateLastProcessReload(): Promise; - abstract getLastProcessReload(): Promise; + abstract getLastProcessReload(): Promise; abstract logout(userId: UserId): Promise; } @@ -123,20 +127,20 @@ 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 lastProcessReloadState: GlobalState; + private fingerprintValidatedState: GlobalState; + private lastProcessReloadState: GlobalState; biometricUnlockEnabled$: Observable; - encryptedClientKeyHalf$: Observable; + encryptedClientKeyHalf$: Observable; requirePasswordOnStart$: Observable; dismissedRequirePasswordOnStartCallout$: Observable; promptCancelled$: Observable; promptAutomatically$: Observable; fingerprintValidated$: Observable; - lastProcessReload$: Observable; + lastProcessReload$: Observable; constructor(private stateProvider: StateProvider) { this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED); @@ -164,7 +168,7 @@ export class DefaultBiometricStateService implements BiometricStateService { this.promptCancelledState.state$, ]).pipe( map(([userId, record]) => { - return record?.[userId] ?? false; + return userId != null ? (record?.[userId] ?? false) : false; }), ); this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY); @@ -188,7 +192,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; @@ -198,7 +202,7 @@ export class DefaultBiometricStateService implements BiometricStateService { combineLatestWith: this.requirePasswordOnStartState.combinedState$, }, ); - if (!value) { + if (!value && currentActiveId) { await this.removeEncryptedClientKeyHalf(currentActiveId); } } @@ -222,7 +226,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) @@ -244,7 +248,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 != null) { + delete data[userId ?? activeUserId]; + } return data; }, { @@ -257,8 +263,10 @@ export class DefaultBiometricStateService implements BiometricStateService { async setUserPromptCancelled(): Promise { await this.promptCancelledState.update( (record, userId) => { - record ??= {}; - record[userId] = true; + if (userId != null) { + record ??= {}; + record[userId] = true; + } return record; }, { @@ -291,13 +299,13 @@ export class DefaultBiometricStateService implements BiometricStateService { await this.lastProcessReloadState.update(() => new Date()); } - async getLastProcessReload(): Promise { + async getLastProcessReload(): Promise { return await firstValueFrom(this.lastProcessReload$); } } function encryptedClientKeyHalfToEncString( - encryptedKeyHalf: EncryptedString | undefined, -): EncString { + encryptedKeyHalf: EncryptedString | null | undefined, +): EncString | null { return encryptedKeyHalf == null ? null : new EncString(encryptedKeyHalf); } diff --git a/libs/key-management/src/biometrics/biometric.state.spec.ts b/libs/key-management/src/biometrics/biometric.state.spec.ts index 94ae5217f47..b961808cea7 100644 --- a/libs/key-management/src/biometrics/biometric.state.spec.ts +++ b/libs/key-management/src/biometrics/biometric.state.spec.ts @@ -1,4 +1,3 @@ -import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyDefinition, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { @@ -21,12 +20,7 @@ describe.each([ [FINGERPRINT_VALIDATED, true], ])( "deserializes state %s", - ( - ...args: - | [UserKeyDefinition, EncryptedString] - | [UserKeyDefinition, boolean] - | [KeyDefinition, boolean] - ) => { + (...args: [UserKeyDefinition | KeyDefinition, unknown]) => { function testDeserialization( keyDefinition: UserKeyDefinition | KeyDefinition, state: T, diff --git a/libs/key-management/src/biometrics/biometric.state.ts b/libs/key-management/src/biometrics/biometric.state.ts index e5ea4fbae3b..277d35c176e 100644 --- a/libs/key-management/src/biometrics/biometric.state.ts +++ b/libs/key-management/src/biometrics/biometric.state.ts @@ -1,16 +1,10 @@ -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { EncryptedString } from "../../../common/src/platform/models/domain/enc-string"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports +import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { KeyDefinition, BIOMETRIC_SETTINGS_DISK, UserKeyDefinition, -} from "../../../common/src/platform/state"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -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. From 5b22758db128a33168c78f37945b14c058e6d416 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 23 Jan 2025 23:07:28 +0000 Subject: [PATCH 2/8] formatting --- apps/desktop/src/services/biometric-message-handler.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts index e9e34884d24..c033d84a392 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.ts @@ -23,7 +23,7 @@ import { } from "@bitwarden/key-management"; import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component"; -import { LegacyMessage , LegacyMessageWrapper } from "../models/native-messaging"; +import { LegacyMessage, LegacyMessageWrapper } from "../models/native-messaging"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; const MessageValidTimeout = 10 * 1000; From 65b93e9bb98f47686897a638e06e327898f6eba0 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 23 Jan 2025 23:11:46 +0000 Subject: [PATCH 3/8] callbacks not null expectations --- apps/browser/src/background/nativeMessaging.background.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 08250c938df..0c417aae7d0 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -379,7 +379,7 @@ export class NativeMessagingBackground { this.connected = false; if (messageId != null && this.callbacks.has(messageId)) { - this.callbacks.get(messageId)?.rejecter("invalidateEncryption"); + this.callbacks.get(messageId)!.rejecter("invalidateEncryption"); } } } From 53574ab5581975edb380c7904a37ddc1f2614aea Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 23 Jan 2025 23:28:17 +0000 Subject: [PATCH 4/8] state nullability expectations updates --- .../services/vault-banners.service.ts | 4 ++-- .../common/src/platform/state/global-state.ts | 6 +++--- .../default-single-user-state.ts | 2 +- .../state/implementations/state-base.ts | 21 +++++++++---------- libs/common/src/platform/state/user-state.ts | 2 +- .../platform/theming/theme-state.service.ts | 6 +++++- .../vault-settings/vault-settings.service.ts | 2 +- .../generator-strategy.abstraction.ts | 2 +- .../new-device-verification-notice.service.ts | 2 +- .../tasks/services/default-task.service.ts | 5 ++++- 10 files changed, 29 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 475cfc2df22..7315d131d35 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -144,7 +144,7 @@ export class VaultBannersService { * * @returns a SingleUserState for the premium banner reprompt state */ - private premiumBannerState(userId: UserId): SingleUserState { + private premiumBannerState(userId: UserId): SingleUserState { return this.stateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY); } @@ -152,7 +152,7 @@ export class VaultBannersService { * * @returns a SingleUserState for the session banners dismissed state */ - private sessionBannerState(userId: UserId): SingleUserState { + private sessionBannerState(userId: UserId): SingleUserState { return this.stateProvider.getUser(userId, BANNERS_DISMISSED_DISK_KEY); } diff --git a/libs/common/src/platform/state/global-state.ts b/libs/common/src/platform/state/global-state.ts index b0f19c53faa..3b65c04a18d 100644 --- a/libs/common/src/platform/state/global-state.ts +++ b/libs/common/src/platform/state/global-state.ts @@ -18,13 +18,13 @@ export interface GlobalState { * Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state. */ update: ( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options?: StateUpdateOptions, - ) => Promise; + ) => Promise; /** * An observable stream of this state, the first emission of this will be the current state on disk * and subsequent updates will be from an update to that state. */ - state$: Observable; + state$: Observable; } diff --git a/libs/common/src/platform/state/implementations/default-single-user-state.ts b/libs/common/src/platform/state/implementations/default-single-user-state.ts index 4cfba1ffa95..1dafd3aecad 100644 --- a/libs/common/src/platform/state/implementations/default-single-user-state.ts +++ b/libs/common/src/platform/state/implementations/default-single-user-state.ts @@ -16,7 +16,7 @@ export class DefaultSingleUserState extends StateBase> implements SingleUserState { - readonly combinedState$: Observable>; + readonly combinedState$: Observable>; constructor( readonly userId: UserId, diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts index f285963d6e6..578720a2281 100644 --- a/libs/common/src/platform/state/implementations/state-base.ts +++ b/libs/common/src/platform/state/implementations/state-base.ts @@ -1,12 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { - Observable, - ReplaySubject, defer, filter, firstValueFrom, merge, + Observable, + ReplaySubject, share, switchMap, tap, @@ -22,7 +22,7 @@ import { ObservableStorageService, } from "../../abstractions/storage.service"; import { DebugOptions } from "../key-definition"; -import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options"; +import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options"; import { getStoredValue } from "./util"; @@ -36,7 +36,7 @@ type KeyDefinitionRequirements = { export abstract class StateBase> { private updatePromise: Promise; - readonly state$: Observable; + readonly state$: Observable; constructor( protected readonly key: StorageKey, @@ -86,9 +86,9 @@ export abstract class StateBase> } async update( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options: StateUpdateOptions = {}, - ): Promise { + ): Promise { options = populateOptionsWithDefault(options); if (this.updatePromise != null) { await this.updatePromise; @@ -96,17 +96,16 @@ export abstract class StateBase> try { this.updatePromise = this.internalUpdate(configureState, options); - const newState = await this.updatePromise; - return newState; + return await this.updatePromise; } finally { this.updatePromise = null; } } private async internalUpdate( - configureState: (state: T, dependency: TCombine) => T, + configureState: (state: T | null, dependency: TCombine) => T | null, options: StateUpdateOptions, - ): Promise { + ): Promise { const currentState = await this.getStateForUpdate(); const combinedDependencies = options.combineLatestWith != null @@ -122,7 +121,7 @@ export abstract class StateBase> return newState; } - protected async doStorageSave(newState: T, oldState: T) { + protected async doStorageSave(newState: T | null, oldState: T) { if (this.keyDefinition.debug.enableUpdateLogging) { this.logService.info( `Updating '${this.key}' from ${oldState == null ? "null" : "non-null"} to ${newState == null ? "null" : "non-null"}`, diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts index 5a72189ab32..05a2a0fbe76 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -12,7 +12,7 @@ export interface UserState { readonly state$: Observable; /** Emits a stream of tuples, with the first element being a user id and the second element being the data for that user. */ - readonly combinedState$: Observable>; + readonly combinedState$: Observable>; } export const activeMarker: unique symbol = Symbol("active"); diff --git a/libs/common/src/platform/theming/theme-state.service.ts b/libs/common/src/platform/theming/theme-state.service.ts index bb146be4927..df2c96c49d0 100644 --- a/libs/common/src/platform/theming/theme-state.service.ts +++ b/libs/common/src/platform/theming/theme-state.service.ts @@ -32,7 +32,11 @@ export class DefaultThemeStateService implements ThemeStateService { map(([theme, isExtensionRefresh]) => { // The extension refresh should not allow for Nord or SolarizedDark // Default the user to their system theme - if (isExtensionRefresh && [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme)) { + if ( + isExtensionRefresh && + theme != null && + [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme) + ) { return ThemeType.System; } diff --git a/libs/common/src/vault/services/vault-settings/vault-settings.service.ts b/libs/common/src/vault/services/vault-settings/vault-settings.service.ts index 85ab3914158..f43b217d0f6 100644 --- a/libs/common/src/vault/services/vault-settings/vault-settings.service.ts +++ b/libs/common/src/vault/services/vault-settings/vault-settings.service.ts @@ -13,7 +13,7 @@ import { * {@link VaultSettingsServiceAbstraction} */ export class VaultSettingsService implements VaultSettingsServiceAbstraction { - private enablePasskeysState: GlobalState = + private enablePasskeysState: GlobalState = this.stateProvider.getGlobal(USER_ENABLE_PASSKEYS); /** * {@link VaultSettingsServiceAbstraction.enablePasskeys$} diff --git a/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts b/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts index 3506c7458e4..263f7758c75 100644 --- a/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts +++ b/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts @@ -17,7 +17,7 @@ export abstract class GeneratorStrategy { * @param userId: identifies the user state to retrieve * @returns the strategy's durable user state */ - durableState: (userId: UserId) => SingleUserState; + durableState: (userId: UserId) => SingleUserState; /** Gets the default options. */ defaults$: (userId: UserId) => Observable; diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts index a925cf09ec3..94dbc62321f 100644 --- a/libs/vault/src/services/new-device-verification-notice.service.ts +++ b/libs/vault/src/services/new-device-verification-notice.service.ts @@ -46,7 +46,7 @@ export const NEW_DEVICE_VERIFICATION_NOTICE_KEY = export class NewDeviceVerificationNoticeService { constructor(private stateProvider: StateProvider) {} - private noticeState(userId: UserId): SingleUserState { + private noticeState(userId: UserId): SingleUserState { return this.stateProvider.getUser(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY); } diff --git a/libs/vault/src/tasks/services/default-task.service.ts b/libs/vault/src/tasks/services/default-task.service.ts index f5c1d95af08..998b2077283 100644 --- a/libs/vault/src/tasks/services/default-task.service.ts +++ b/libs/vault/src/tasks/services/default-task.service.ts @@ -87,7 +87,10 @@ export class DefaultTaskService implements TaskService { * @param tasks * @private */ - private updateTaskState(userId: UserId, tasks: SecurityTaskData[]): Promise { + private updateTaskState( + userId: UserId, + tasks: SecurityTaskData[], + ): Promise { return this.taskState(userId).update(() => tasks); } } From e240947ad800b811e4d9c7173a2fb001efd8fbae Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 24 Jan 2025 09:04:10 +0000 Subject: [PATCH 5/8] unit tests fix --- .../biometric-message-handler.service.spec.ts | 32 +++++++++++++++++-- libs/common/spec/fake-account-service.ts | 4 +-- .../biometric-state.service.spec.ts | 4 +-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/services/biometric-message-handler.service.spec.ts b/apps/desktop/src/services/biometric-message-handler.service.spec.ts index a7f0f555ca2..bfe7491efc4 100644 --- a/apps/desktop/src/services/biometric-message-handler.service.spec.ts +++ b/apps/desktop/src/services/biometric-message-handler.service.spec.ts @@ -2,7 +2,7 @@ import { NgZone } from "@angular/core"; import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -27,7 +27,7 @@ import { BiometricMessageHandlerService } from "./biometric-message-handler.serv const SomeUser = "SomeUser" as UserId; const AnotherUser = "SomeOtherUser" as UserId; -const accounts = { +const accounts: Record = { [SomeUser]: { name: "some user", email: "some.user@example.com", @@ -108,6 +108,30 @@ describe("BiometricMessageHandlerService", () => { }); describe("setup encryption", () => { + it("should ignore when public key missing in message", async () => { + await service.handleMessage({ + appId: "appId", + message: { + command: "setupEncryption", + messageId: 0, + userId: "unknownUser" as UserId, + }, + }); + expect((global as any).ipc.platform.nativeMessaging.sendMessage).not.toHaveBeenCalled(); + }); + + it("should ignore when user id missing in message", async () => { + await service.handleMessage({ + appId: "appId", + message: { + command: "setupEncryption", + messageId: 0, + publicKey: Utils.fromUtf8ToB64("publicKey"), + }, + }); + expect((global as any).ipc.platform.nativeMessaging.sendMessage).not.toHaveBeenCalled(); + }); + it("should reject when user is not in app", async () => { await service.handleMessage({ appId: "appId", @@ -115,6 +139,7 @@ describe("BiometricMessageHandlerService", () => { command: "setupEncryption", messageId: 0, userId: "unknownUser" as UserId, + publicKey: Utils.fromUtf8ToB64("publicKey"), }, }); expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({ @@ -362,12 +387,15 @@ describe("BiometricMessageHandlerService", () => { // always reload when another user is active than the requested one [SomeUser, AuthenticationStatus.Unlocked, AnotherUser, false, true], [SomeUser, AuthenticationStatus.Locked, AnotherUser, false, true], + // don't reload when no active user + [null, AuthenticationStatus.Unlocked, AnotherUser, false, false], // don't reload in dev mode [SomeUser, AuthenticationStatus.Unlocked, SomeUser, true, false], [SomeUser, AuthenticationStatus.Locked, SomeUser, true, false], [SomeUser, AuthenticationStatus.Unlocked, AnotherUser, true, false], [SomeUser, AuthenticationStatus.Locked, AnotherUser, true, false], + [null, AuthenticationStatus.Unlocked, AnotherUser, true, false], ]; it.each(testCases)( diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index 05e44d5db18..2a84bec91be 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { mock } from "jest-mock-extended"; -import { ReplaySubject, combineLatest, map } from "rxjs"; +import { ReplaySubject, combineLatest, map, Observable } from "rxjs"; import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; import { UserId } from "../src/types/guid"; @@ -52,7 +52,7 @@ export class FakeAccountService implements AccountService { }), ); } - get nextUpAccount$() { + get nextUpAccount$(): Observable { return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe( map(([accounts, activeAccount, sortedUserIds]) => { const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); 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 850f461d908..338826dfcba 100644 --- a/libs/key-management/src/biometrics/biometric-state.service.spec.ts +++ b/libs/key-management/src/biometrics/biometric-state.service.spec.ts @@ -7,11 +7,9 @@ import { FakeStateProvider, FakeGlobalState, FakeSingleUserState, -} from "@bitwarden/common/spec"; -import { FakeAccountService, mockAccountServiceWith, -} from "@bitwarden/common/spec/fake-account-service"; +} from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService, DefaultBiometricStateService } from "./biometric-state.service"; From 6d0f0066b53a070bc387c0ce6293aee50037f030 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 28 Jan 2025 15:13:20 +0000 Subject: [PATCH 6/8] secure channel naming, explicit null check on messageId --- .../background/nativeMessaging.background.ts | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 0c417aae7d0..8591a43bd6a 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -77,7 +77,7 @@ export class NativeMessagingBackground { private port?: browser.runtime.Port | chrome.runtime.Port; private appId?: string; - private secure?: SecureChannel; + private secureChannel?: SecureChannel; private messageId = 0; private callbacks = new Map(); @@ -161,13 +161,13 @@ export class NativeMessagingBackground { if (message.sharedSecret == null) { this.logService.info( - "[Native Messaging IPC] Unable to create secure channel, no shared secret", + "[Native Messaging IPC] Unable to create secureChannel channel, no shared secret", ); return; } - if (this.secure == null) { + if (this.secureChannel == null) { this.logService.info( - "[Native Messaging IPC] Unable to create secure channel, no secure communication setup", + "[Native Messaging IPC] Unable to create secureChannel channel, no secureChannel communication setup", ); return; } @@ -175,11 +175,11 @@ export class NativeMessagingBackground { const encrypted = Utils.fromB64ToArray(message.sharedSecret); const decrypted = await this.cryptoFunctionService.rsaDecrypt( encrypted, - this.secure.privateKey, + this.secureChannel.privateKey, HashAlgorithmForEncryption, ); - this.secure.sharedSecret = new SymmetricCryptoKey(decrypted); + this.secureChannel.sharedSecret = new SymmetricCryptoKey(decrypted); this.logService.info("[Native Messaging IPC] Secure channel established"); if ("messageId" in message) { @@ -190,7 +190,7 @@ export class NativeMessagingBackground { this.isConnectedToOutdatedDesktopClient = true; } - this.secure.setupResolve(); + this.secureChannel.setupResolve(); break; } case "invalidateEncryption": @@ -202,7 +202,7 @@ export class NativeMessagingBackground { "[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...", ); - this.secure = undefined; + this.secureChannel = undefined; this.connected = false; if (message.messageId != null) { @@ -235,7 +235,7 @@ export class NativeMessagingBackground { break; } case "wrongUserId": - if (message.messageId) { + if (message.messageId != null) { if (this.callbacks.has(message.messageId)) { this.callbacks.get(message.messageId)?.rejecter({ message: "wrongUserId", @@ -265,7 +265,7 @@ export class NativeMessagingBackground { error = chrome.runtime.lastError?.message; } - this.secure = undefined; + this.secureChannel = undefined; this.connected = false; this.logService.error("NativeMessaging port disconnected because of error: " + error); @@ -346,11 +346,14 @@ export class NativeMessagingBackground { } async encryptMessage(message: Message) { - if (this.secure?.sharedSecret == null) { + if (this.secureChannel?.sharedSecret == null) { await this.secureCommunication(); } - return await this.encryptService.encrypt(JSON.stringify(message), this.secure!.sharedSecret!); + return await this.encryptService.encrypt( + JSON.stringify(message), + this.secureChannel!.sharedSecret!, + ); } private postMessage(message: OuterMessage, messageId?: number) { @@ -375,7 +378,7 @@ export class NativeMessagingBackground { "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.", ); - this.secure = undefined; + this.secureChannel = undefined; this.connected = false; if (messageId != null && this.callbacks.has(messageId)) { @@ -387,13 +390,13 @@ export class NativeMessagingBackground { private async onMessage(rawMessage: ReceiveMessage | EncString) { let message: ReceiveMessage; if (!this.platformUtilsService.isSafari()) { - if (this.secure?.sharedSecret == null) { + if (this.secureChannel?.sharedSecret == null) { return; } message = JSON.parse( await this.encryptService.decryptToUtf8( rawMessage as EncString, - this.secure.sharedSecret, + this.secureChannel.sharedSecret, "ipc-desktop-ipc-channel-key", ), ); @@ -445,7 +448,7 @@ export class NativeMessagingBackground { }); return new Promise((resolve) => { - this.secure = { + this.secureChannel = { publicKey, privateKey, setupResolve: resolve, @@ -464,10 +467,13 @@ export class NativeMessagingBackground { } private async showFingerprintDialog() { - if (this.secure?.publicKey == null) { + if (this.secureChannel?.publicKey == null) { return; } - const fingerprint = await this.keyService.getFingerprint(this.appId!, this.secure.publicKey); + const fingerprint = await this.keyService.getFingerprint( + this.appId!, + this.secureChannel.publicKey, + ); this.messagingService.send("showNativeMessagingFingerprintDialog", { fingerprint: fingerprint, From 8aa0f7275d9b88489aa6639c5f663f3b1f3a188d Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 5 Feb 2025 13:14:01 +0000 Subject: [PATCH 7/8] revert null for getUser, getGlobal in state.provider.ts --- .../vault-banners/services/vault-banners.service.ts | 4 ++-- libs/common/src/platform/state/state.provider.ts | 7 ++----- .../services/vault-settings/vault-settings.service.ts | 2 +- .../src/biometrics/biometric-state.service.ts | 6 +++--- .../src/abstractions/generator-strategy.abstraction.ts | 2 +- .../src/services/new-device-verification-notice.service.ts | 2 +- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 7315d131d35..475cfc2df22 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -144,7 +144,7 @@ export class VaultBannersService { * * @returns a SingleUserState for the premium banner reprompt state */ - private premiumBannerState(userId: UserId): SingleUserState { + private premiumBannerState(userId: UserId): SingleUserState { return this.stateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY); } @@ -152,7 +152,7 @@ export class VaultBannersService { * * @returns a SingleUserState for the session banners dismissed state */ - private sessionBannerState(userId: UserId): SingleUserState { + private sessionBannerState(userId: UserId): SingleUserState { return this.stateProvider.getUser(userId, BANNERS_DISMISSED_DISK_KEY); } diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index a37726c60a7..dc8cb3e9359 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -68,13 +68,10 @@ 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/vault/services/vault-settings/vault-settings.service.ts b/libs/common/src/vault/services/vault-settings/vault-settings.service.ts index f43b217d0f6..85ab3914158 100644 --- a/libs/common/src/vault/services/vault-settings/vault-settings.service.ts +++ b/libs/common/src/vault/services/vault-settings/vault-settings.service.ts @@ -13,7 +13,7 @@ import { * {@link VaultSettingsServiceAbstraction} */ export class VaultSettingsService implements VaultSettingsServiceAbstraction { - private enablePasskeysState: GlobalState = + private enablePasskeysState: GlobalState = this.stateProvider.getGlobal(USER_ENABLE_PASSKEYS); /** * {@link VaultSettingsServiceAbstraction.enablePasskeys$} diff --git a/libs/key-management/src/biometrics/biometric-state.service.ts b/libs/key-management/src/biometrics/biometric-state.service.ts index 80ae273b416..837c266bee5 100644 --- a/libs/key-management/src/biometrics/biometric-state.service.ts +++ b/libs/key-management/src/biometrics/biometric-state.service.ts @@ -129,10 +129,10 @@ export class DefaultBiometricStateService implements BiometricStateService { private requirePasswordOnStartState: ActiveUserState; private encryptedClientKeyHalfState: ActiveUserState; private dismissedRequirePasswordOnStartCalloutState: ActiveUserState; - private promptCancelledState: GlobalState | null>; + private promptCancelledState: GlobalState>; private promptAutomaticallyState: ActiveUserState; - private fingerprintValidatedState: GlobalState; - private lastProcessReloadState: GlobalState; + private fingerprintValidatedState: GlobalState; + private lastProcessReloadState: GlobalState; biometricUnlockEnabled$: Observable; encryptedClientKeyHalf$: Observable; requirePasswordOnStart$: Observable; diff --git a/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts b/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts index 263f7758c75..3506c7458e4 100644 --- a/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts +++ b/libs/tools/generator/core/src/abstractions/generator-strategy.abstraction.ts @@ -17,7 +17,7 @@ export abstract class GeneratorStrategy { * @param userId: identifies the user state to retrieve * @returns the strategy's durable user state */ - durableState: (userId: UserId) => SingleUserState; + durableState: (userId: UserId) => SingleUserState; /** Gets the default options. */ defaults$: (userId: UserId) => Observable; diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts index 94dbc62321f..a925cf09ec3 100644 --- a/libs/vault/src/services/new-device-verification-notice.service.ts +++ b/libs/vault/src/services/new-device-verification-notice.service.ts @@ -46,7 +46,7 @@ export const NEW_DEVICE_VERIFICATION_NOTICE_KEY = export class NewDeviceVerificationNoticeService { constructor(private stateProvider: StateProvider) {} - private noticeState(userId: UserId): SingleUserState { + private noticeState(userId: UserId): SingleUserState { return this.stateProvider.getUser(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY); } From e5ff391597aacf9c04a54fa7166d048c44715311 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 5 Feb 2025 13:16:08 +0000 Subject: [PATCH 8/8] revert null for getUser, getGlobal in state.provider.ts --- libs/common/spec/fake-state-provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index c2e850b7726..9f72ccada55 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -240,11 +240,11 @@ export class FakeStateProvider implements StateProvider { return this.activeUser.get(userKeyDefinition); } - getGlobal(keyDefinition: KeyDefinition): GlobalState { + getGlobal(keyDefinition: KeyDefinition): GlobalState { return this.global.get(keyDefinition); } - getUser(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { + getUser(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { return this.singleUser.get(userId, userKeyDefinition); }