Skip to content

Commit

Permalink
PM-14445: TS strict for Key Management Biometrics
Browse files Browse the repository at this point in the history
  • Loading branch information
mzieniukbw committed Jan 23, 2025
1 parent 382a2a0 commit ac11768
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 189 deletions.
158 changes: 96 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 @@ 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<number, Callback>();
Expand Down Expand Up @@ -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;

Expand All @@ -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();
Expand All @@ -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");
Expand All @@ -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) {
Expand All @@ -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": {
Expand All @@ -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);
}
}
});

Expand All @@ -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));
});
});
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -346,34 +367,38 @@ 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) {
this.logService.info(
"[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) {
Expand All @@ -390,24 +415,24 @@ 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);
}
}

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 +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) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit ac11768

Please sign in to comment.