Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
164 changes: 102 additions & 62 deletions apps/browser/src/background/nativeMessaging.background.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -57,26 +55,29 @@
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;

Check warning on line 76 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L76

Added line #L76 was not covered by tests
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>();
Expand Down Expand Up @@ -108,11 +109,13 @@

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;

Check warning on line 113 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L112-L113

Added lines #L112 - L113 were not covered by tests
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;

Check warning on line 118 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L117-L118

Added lines #L117 - L118 were not covered by tests

this.connecting = true;

Expand All @@ -131,7 +134,8 @@
connectedCallback();
}

this.port.onMessage.addListener(async (message: ReceiveMessageOuter) => {
port.onMessage.addListener(async (messageRaw: unknown) => {
const message = messageRaw as ReceiveMessageOuter;

Check warning on line 138 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L137-L138

Added lines #L137 - L138 were not covered by tests
switch (message.command) {
case "connected":
connectedCallback();
Expand All @@ -142,7 +146,7 @@
reject(new Error("startDesktop"));
}
this.connected = false;
this.port.disconnect();
port.disconnect();

Check warning on line 149 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L149

Added line #L149 was not covered by tests
// reject all
for (const callback of this.callbacks.values()) {
callback.rejecter("disconnected");
Expand All @@ -151,18 +155,31 @@
break;
case "setupEncryption": {
// Ignore since it belongs to another device
if (message.appId !== this.appId) {
if (message.appId !== appId) {
return;

Check warning on line 159 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L159

Added line #L159 was not covered by tests
}

if (message.sharedSecret == null) {
this.logService.info(

Check warning on line 163 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L163

Added line #L163 was not covered by tests
"[Native Messaging IPC] Unable to create secureChannel channel, no shared secret",
);
return;

Check warning on line 166 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L166

Added line #L166 was not covered by tests
}
if (this.secureChannel == null) {
this.logService.info(

Check warning on line 169 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L169

Added line #L169 was not covered by tests
"[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);

Check warning on line 182 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L182

Added line #L182 was not covered by tests
this.logService.info("[Native Messaging IPC] Secure channel established");

if ("messageId" in message) {
Expand All @@ -173,26 +190,27 @@
this.isConnectedToOutdatedDesktopClient = true;
}

this.secureSetupResolve();
this.secureChannel.setupResolve();

Check warning on line 193 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L193

Added line #L193 was not covered by tests
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;

Check warning on line 205 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L205

Added line #L205 was not covered by tests
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": {
Expand All @@ -217,21 +235,25 @@
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);

Check warning on line 255 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L255

Added line #L255 was not covered by tests
}
}
});

Expand All @@ -240,16 +262,15 @@
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;

Check warning on line 268 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L268

Added line #L268 was not covered by tests
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));
});
});
Expand Down Expand Up @@ -293,13 +314,13 @@
);
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({

Check warning on line 323 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L323

Added line #L323 was not covered by tests
message: "timeout",
});
this.callbacks.delete(messageId);
Expand All @@ -320,16 +341,19 @@
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) });

Check warning on line 344 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L344

Added line #L344 was not covered by tests
}
}

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(

Check warning on line 353 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L353

Added line #L353 was not covered by tests
JSON.stringify(message),
this.secureChannel!.sharedSecret!,
);
}

private postMessage(message: OuterMessage, messageId?: number) {
Expand All @@ -346,34 +370,38 @@
mac: message.message.mac,
};
}
this.port.postMessage(msg);
this.port!.postMessage(msg);

Check warning on line 373 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L373

Added line #L373 was not covered by tests
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.logService.info(
"[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.",
);

this.sharedSecret = null;
this.privateKey = null;
this.secureChannel = undefined;

Check warning on line 381 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L381

Added line #L381 was not covered by tests
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");

Check warning on line 385 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L385

Added line #L385 was not covered by tests
}
}
}

private async onMessage(rawMessage: ReceiveMessage | EncString) {
let message = rawMessage as ReceiveMessage;
let message: ReceiveMessage;
if (!this.platformUtilsService.isSafari()) {
if (this.secureChannel?.sharedSecret == null) {
return;

Check warning on line 394 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L394

Added line #L394 was not covered by tests
}
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;

Check warning on line 404 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L404

Added line #L404 was not covered by tests
}

if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
Expand All @@ -390,24 +418,24 @@
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;

Check warning on line 421 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L421

Added line #L421 was not covered by tests
if (messageId != null) {
const resolver = this.callbacks.get(messageId);
this.callbacks.delete(messageId);
resolver!.resolver(message);

Check warning on line 425 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L423-L425

Added lines #L423 - L425 were not covered by tests
}
return;
}

if (this.callbacks.has(messageId)) {
this.callbacks.get(messageId).resolver(message);
this.callbacks.get(messageId)!.resolver(message);

Check warning on line 431 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L431

Added line #L431 was not covered by tests
} else {
this.logService.info("[Native Messaging IPC] Received message without a callback", message);
}
}

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.
Expand All @@ -419,7 +447,13 @@
messageId: this.messageId++,
});

return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
return new Promise((resolve) => {
this.secureChannel = {

Check warning on line 451 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L450-L451

Added lines #L450 - L451 were not covered by tests
publicKey,
privateKey,
setupResolve: resolve,
};
});
}

private async sendUnencrypted(message: Message) {
Expand All @@ -429,11 +463,17 @@

message.timestamp = Date.now();

this.postMessage({ appId: this.appId, message: message });
this.postMessage({ appId: this.appId!, message: message });

Check warning on line 466 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L466

Added line #L466 was not covered by tests
}

private async showFingerprintDialog() {
const fingerprint = await this.keyService.getFingerprint(this.appId, this.publicKey);
if (this.secureChannel?.publicKey == null) {
return;

Check warning on line 471 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L471

Added line #L471 was not covered by tests
}
const fingerprint = await this.keyService.getFingerprint(

Check warning on line 473 in apps/browser/src/background/nativeMessaging.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/nativeMessaging.background.ts#L473

Added line #L473 was not covered by tests
this.appId!,
this.secureChannel.publicKey,
);

this.messagingService.send("showNativeMessagingFingerprintDialog", {
fingerprint: fingerprint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
case BiometricAction.GetStatusForUser:
return await this.biometricService.getBiometricsStatusForUser(message.userId as UserId);
case BiometricAction.SetKeyForUser:
if (message.key == null) {
return;

Check warning on line 36 in apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts#L36

Added line #L36 was not covered by tests
}
return await this.biometricService.setBiometricProtectedUnlockKeyForUser(
message.userId as UserId,
message.key,
Expand All @@ -41,6 +44,9 @@
message.userId as UserId,
);
case BiometricAction.SetClientKeyHalf:
if (message.key == null) {
return;

Check warning on line 48 in apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts#L48

Added line #L48 was not covered by tests
}
return await this.biometricService.setClientKeyHalfForUser(
message.userId as UserId,
message.key,
Expand Down
Loading
Loading