1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-14445] TS strict for Key Management Biometrics (#13039)

* PM-14445: TS strict for Key Management Biometrics

* formatting

* callbacks not null expectations

* state nullability expectations updates

* unit tests fix

* secure channel naming, explicit null check on messageId

* revert null for getUser, getGlobal in state.provider.ts

* revert null for getUser, getGlobal in state.provider.ts
This commit is contained in:
Maciej Zieniuk
2025-02-10 13:31:19 +01:00
committed by GitHub
parent 40e8c88d77
commit 7e2e604439
23 changed files with 307 additions and 204 deletions

View File

@@ -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 secureChannel?: SecureChannel;
private messageId = 0;
private callbacks = new Map<number, Callback>();
@@ -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<void>((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 secureChannel channel, no shared secret",
);
return;
}
if (this.secureChannel == null) {
this.logService.info(
"[Native Messaging IPC] Unable to create secureChannel channel, no secureChannel communication setup",
);
return;
}
const encrypted = Utils.fromB64ToArray(message.sharedSecret);
const decrypted = await this.cryptoFunctionService.rsaDecrypt(
encrypted,
this.privateKey,
this.secureChannel.privateKey,
HashAlgorithmForEncryption,
);
this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.secureChannel.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.secureChannel.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.secureChannel = 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 != null) {
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.secureChannel = 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,19 @@ 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.secureChannel?.sharedSecret == null) {
await this.secureCommunication();
}
return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret);
return await this.encryptService.encrypt(
JSON.stringify(message),
this.secureChannel!.sharedSecret!,
);
}
private postMessage(message: OuterMessage, messageId?: number) {
@@ -346,7 +370,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 +378,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.secureChannel = 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.secureChannel?.sharedSecret == null) {
return;
}
message = JSON.parse(
await this.encryptService.decryptToUtf8(
rawMessage as EncString,
this.sharedSecret,
this.secureChannel.sharedSecret,
"ipc-desktop-ipc-channel-key",
),
);
} else {
message = rawMessage as ReceiveMessage;
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
@@ -390,15 +418,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 +436,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 +447,13 @@ export class NativeMessagingBackground {
messageId: this.messageId++,
});
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
return new Promise((resolve) => {
this.secureChannel = {
publicKey,
privateKey,
setupResolve: resolve,
};
});
}
private async sendUnencrypted(message: Message) {
@@ -429,11 +463,17 @@ 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.secureChannel?.publicKey == null) {
return;
}
const fingerprint = await this.keyService.getFingerprint(
this.appId!,
this.secureChannel.publicKey,
);
this.messagingService.send("showNativeMessagingFingerprintDialog", {
fingerprint: fingerprint,

View File

@@ -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,

View File

@@ -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<UserKey | null> {
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<void> {

View File

@@ -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,

View File

@@ -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<boolean> {
@@ -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;

View File

@@ -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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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<UserId, AccountInfo> = {
[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)(

View File

@@ -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<ConnectedApp> {
async get(appId: string): Promise<ConnectedApp | null> {
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;