mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[SG-520] Native messaging handler (#3566)
* [SG-523] Base test runner app for native messages (#3269) * Base test runner app for native messages * Remove default test script * Add case for canceled status * Modify to allow usage of libs crypto services and functions * Small adjustments * Handshake request (#3277) * Handshake request * Fix capitalization * Update info text * lock node-ipc to 9.2.1 * [SG-569] Native Messaging settings bug (#3285) * Fix bug where updating setting wasn't starting the native messaging listener * Update test runner error message * [SG-532] Implement Status command in Native Messaging Service (#3310) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Remove in progress file (merge error) * Move models to their own folder and add index.ts * Remove file that got un-deleted * Remove file that will be added in separate command * Fix imports that got borked * [SG-533] Implement bw-credential-retrieval (#3334) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Add error handling for passing a bad public key to handshake * [SG-534] and [SG-535] Implement Credential Create and Update commands (#3342) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Add bw-credential-create * Better response handling in test runner * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * bw-cipher-create move type into its own file * Use LogUtils for all logging * Implement bw-credential-update * Give naming conventions for types * Rename file correctly * Update handleEncyptedMessage with EncString changes * [SG-626] Fix Desktop app not showing updated credentials from native messages (#3380) * Add MessagingService to send messages on login create and update * Add `not-active-user` error to create and update and other refactors * [SG-536] Implement bw-generate-password (#3370) * implement bw-generate-password * Fix merge conflict resolution errors * Update apps/desktop/native-messaging-test-runner/src/bw-generate-password.ts Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Logging improvements * Add NativeMessagingVersion enum * Add version check in NativeMessagingHandler Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Refactor account status checks and check for locked state in generate command (#3461) * Add feawture flag to show/hide ddg setting (#3506) * [SG-649] Add confirmation dialog and tweak shared key retrieval (#3451) * Add confirmation dialog when completing handshake * Copy updates for dialog * HandshakeResponse type fixes * Add longer timeout for handshake command * [SG-663] RefactorNativeMessagingHandlerService and strengthen typing (#3551) * NativeMessageHandlerService refactor and additional types * Return empty array if no uri to retrieve command * Move commands from test runner into a separate folder * Fix bug where confirmation dialog messes with styling * Enable DDG feature * Fix generated password not saving to history * Take credentialId as parameter to update * Add applicationName to handshake payload * Add warning text to confirmation modal Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
|
||||
import { CredentialCreatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
|
||||
import { LogUtils } from "../logUtils";
|
||||
import NativeMessageService from "../nativeMessageService";
|
||||
import * as config from "../variables";
|
||||
|
||||
const argv: any = yargs(hideBin(process.argv)).option("name", {
|
||||
alias: "n",
|
||||
demand: true,
|
||||
describe: "Name that the created login will be given",
|
||||
type: "string",
|
||||
}).argv;
|
||||
|
||||
const { name } = argv;
|
||||
|
||||
(async () => {
|
||||
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
|
||||
// Handshake
|
||||
LogUtils.logInfo("Sending Handshake");
|
||||
const handshakeResponse = await nativeMessageService.sendHandshake(
|
||||
config.testRsaPublicKey,
|
||||
config.applicationName
|
||||
);
|
||||
|
||||
if (!handshakeResponse.status) {
|
||||
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
|
||||
nativeMessageService.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get active account userId
|
||||
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
|
||||
|
||||
const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0];
|
||||
if (activeUser === undefined) {
|
||||
LogUtils.logError("No active or unlocked user");
|
||||
}
|
||||
LogUtils.logInfo("Active userId: " + activeUser.id);
|
||||
|
||||
LogUtils.logSuccess("Handshake success response");
|
||||
const response = await nativeMessageService.credentialCreation(handshakeResponse.sharedKey, {
|
||||
name: name,
|
||||
userName: "SuperAwesomeUser",
|
||||
password: "dolhpin",
|
||||
uri: "google.com",
|
||||
userId: activeUser.id,
|
||||
} as CredentialCreatePayload);
|
||||
|
||||
if (response.payload.status === "failure") {
|
||||
LogUtils.logError("Failure response returned ");
|
||||
} else if (response.payload.status === "success") {
|
||||
LogUtils.logSuccess("Success response returned ");
|
||||
} else if (response.payload.error === "locked") {
|
||||
LogUtils.logError("Error: vault is locked");
|
||||
} else {
|
||||
LogUtils.logWarning("Other response: ", response);
|
||||
}
|
||||
|
||||
nativeMessageService.disconnect();
|
||||
})();
|
||||
@@ -0,0 +1,46 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
|
||||
import { LogUtils } from "../logUtils";
|
||||
import NativeMessageService from "../nativeMessageService";
|
||||
import * as config from "../variables";
|
||||
|
||||
const argv: any = yargs(hideBin(process.argv)).option("uri", {
|
||||
alias: "u",
|
||||
demand: true,
|
||||
describe: "The uri to retrieve logins for",
|
||||
type: "string",
|
||||
}).argv;
|
||||
|
||||
const { uri } = argv;
|
||||
|
||||
(async () => {
|
||||
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
|
||||
// Handshake
|
||||
LogUtils.logInfo("Sending Handshake");
|
||||
const handshakeResponse = await nativeMessageService.sendHandshake(
|
||||
config.testRsaPublicKey,
|
||||
config.applicationName
|
||||
);
|
||||
|
||||
if (!handshakeResponse.status) {
|
||||
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
|
||||
nativeMessageService.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.logSuccess("Handshake success response");
|
||||
const response = await nativeMessageService.credentialRetrieval(handshakeResponse.sharedKey, uri);
|
||||
|
||||
if (response.payload.error != null) {
|
||||
LogUtils.logError("Error response returned: ", response.payload.error);
|
||||
} else {
|
||||
LogUtils.logSuccess("Credentials returned ", response);
|
||||
}
|
||||
|
||||
nativeMessageService.disconnect();
|
||||
})();
|
||||
@@ -0,0 +1,89 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
|
||||
import { CredentialUpdatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
|
||||
import { LogUtils } from "../logUtils";
|
||||
import NativeMessageService from "../nativeMessageService";
|
||||
import * as config from "../variables";
|
||||
|
||||
// Command line arguments
|
||||
const argv: any = yargs(hideBin(process.argv))
|
||||
.option("name", {
|
||||
alias: "n",
|
||||
demand: true,
|
||||
describe: "Name that the updated login will be given",
|
||||
type: "string",
|
||||
})
|
||||
.option("username", {
|
||||
alias: "u",
|
||||
demand: true,
|
||||
describe: "Username that the login will be given",
|
||||
type: "string",
|
||||
})
|
||||
.option("password", {
|
||||
alias: "p",
|
||||
demand: true,
|
||||
describe: "Password that the login will be given",
|
||||
type: "string",
|
||||
})
|
||||
.option("uri", {
|
||||
demand: true,
|
||||
describe: "Uri that the login will be given",
|
||||
type: "string",
|
||||
})
|
||||
.option("credentialId", {
|
||||
demand: true,
|
||||
describe: "GUID of the credential to update",
|
||||
type: "string",
|
||||
}).argv;
|
||||
|
||||
const { name, username, password, uri, credentialId } = argv;
|
||||
|
||||
(async () => {
|
||||
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
|
||||
// Handshake
|
||||
LogUtils.logInfo("Sending Handshake");
|
||||
const handshakeResponse = await nativeMessageService.sendHandshake(
|
||||
config.testRsaPublicKey,
|
||||
config.applicationName
|
||||
);
|
||||
|
||||
if (!handshakeResponse.status) {
|
||||
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
|
||||
nativeMessageService.disconnect();
|
||||
return;
|
||||
}
|
||||
LogUtils.logSuccess("Handshake success response");
|
||||
|
||||
// Get active account userId
|
||||
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
|
||||
|
||||
const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0];
|
||||
if (activeUser === undefined) {
|
||||
LogUtils.logError("No active or unlocked user");
|
||||
}
|
||||
LogUtils.logInfo("Active userId: " + activeUser.id);
|
||||
|
||||
const response = await nativeMessageService.credentialUpdate(handshakeResponse.sharedKey, {
|
||||
name: name,
|
||||
password: password,
|
||||
userName: username,
|
||||
uri: uri,
|
||||
userId: activeUser.id,
|
||||
credentialId: credentialId,
|
||||
} as CredentialUpdatePayload);
|
||||
|
||||
if (response.payload.status === "failure") {
|
||||
LogUtils.logError("Failure response returned ");
|
||||
} else if (response.payload.status === "success") {
|
||||
LogUtils.logSuccess("Success response returned ");
|
||||
} else {
|
||||
LogUtils.logWarning("Other response: ", response);
|
||||
}
|
||||
|
||||
nativeMessageService.disconnect();
|
||||
})();
|
||||
@@ -0,0 +1,46 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
|
||||
import { LogUtils } from "../logUtils";
|
||||
import NativeMessageService from "../nativeMessageService";
|
||||
import * as config from "../variables";
|
||||
|
||||
const argv: any = yargs(hideBin(process.argv)).option("userId", {
|
||||
alias: "u",
|
||||
demand: true,
|
||||
describe: "UserId to generate password for",
|
||||
type: "string",
|
||||
}).argv;
|
||||
|
||||
const { userId } = argv;
|
||||
|
||||
(async () => {
|
||||
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
|
||||
// Handshake
|
||||
LogUtils.logInfo("Sending Handshake");
|
||||
const handshakeResponse = await nativeMessageService.sendHandshake(
|
||||
config.testRsaPublicKey,
|
||||
config.applicationName
|
||||
);
|
||||
|
||||
if (!handshakeResponse.status) {
|
||||
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
|
||||
nativeMessageService.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.logSuccess("Handshake success response");
|
||||
const response = await nativeMessageService.generatePassword(handshakeResponse.sharedKey, userId);
|
||||
|
||||
if (response.payload.error != null) {
|
||||
LogUtils.logError("Error response returned: ", response.payload.error);
|
||||
} else {
|
||||
LogUtils.logSuccess("Response: ", response);
|
||||
}
|
||||
|
||||
nativeMessageService.disconnect();
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
|
||||
import { LogUtils } from "../logUtils";
|
||||
import NativeMessageService from "../nativeMessageService";
|
||||
import * as config from "../variables";
|
||||
|
||||
(async () => {
|
||||
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
|
||||
|
||||
const response = await nativeMessageService.sendHandshake(
|
||||
config.testRsaPublicKey,
|
||||
config.applicationName
|
||||
);
|
||||
LogUtils.logSuccess("Received response to handshake request");
|
||||
if (response.status) {
|
||||
LogUtils.logSuccess("Handshake success response");
|
||||
} else if (response.error === "canceled") {
|
||||
LogUtils.logWarning("Handshake canceled by user");
|
||||
} else {
|
||||
LogUtils.logError("Handshake failure response");
|
||||
}
|
||||
nativeMessageService.disconnect();
|
||||
})();
|
||||
@@ -0,0 +1,29 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
|
||||
import { LogUtils } from "../logUtils";
|
||||
import NativeMessageService from "../nativeMessageService";
|
||||
import * as config from "../variables";
|
||||
|
||||
(async () => {
|
||||
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
|
||||
|
||||
LogUtils.logInfo("Sending Handshake");
|
||||
const handshakeResponse = await nativeMessageService.sendHandshake(
|
||||
config.testRsaPublicKey,
|
||||
config.applicationName
|
||||
);
|
||||
LogUtils.logSuccess("Received response to handshake request");
|
||||
|
||||
if (!handshakeResponse.status) {
|
||||
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
|
||||
nativeMessageService.disconnect();
|
||||
return;
|
||||
}
|
||||
LogUtils.logSuccess("Handshake success response");
|
||||
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
|
||||
|
||||
LogUtils.logSuccess("Status output is: ", status);
|
||||
nativeMessageService.disconnect();
|
||||
})();
|
||||
26
apps/desktop/native-messaging-test-runner/src/deferred.ts
Normal file
26
apps/desktop/native-messaging-test-runner/src/deferred.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Wrapper for a promise that we can await the promise in one case
|
||||
// while allowing an unrelated event to fulfill it elsewhere.
|
||||
export default class Deferred<T> {
|
||||
private promise: Promise<T>;
|
||||
private resolver: (T?) => void;
|
||||
private rejecter: (Error?) => void;
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolver = resolve;
|
||||
this.rejecter = reject;
|
||||
});
|
||||
}
|
||||
|
||||
resolve(value?: T) {
|
||||
this.resolver(value);
|
||||
}
|
||||
|
||||
reject(error?: Error) {
|
||||
this.rejecter(error);
|
||||
}
|
||||
|
||||
getPromise(): Promise<T> {
|
||||
return this.promise;
|
||||
}
|
||||
}
|
||||
166
apps/desktop/native-messaging-test-runner/src/ipcService.ts
Normal file
166
apps/desktop/native-messaging-test-runner/src/ipcService.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { homedir } from "os";
|
||||
|
||||
import * as NodeIPC from "node-ipc";
|
||||
|
||||
import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon";
|
||||
import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse";
|
||||
|
||||
import Deferred from "./deferred";
|
||||
import { race } from "./race";
|
||||
|
||||
NodeIPC.config.id = "native-messaging-test-runner";
|
||||
NodeIPC.config.maxRetries = 0;
|
||||
NodeIPC.config.silent = true;
|
||||
|
||||
const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`;
|
||||
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
|
||||
|
||||
export type MessageHandler = (MessageCommon) => void;
|
||||
|
||||
export enum IPCConnectionState {
|
||||
Disconnected = "disconnected",
|
||||
Connecting = "connecting",
|
||||
Connected = "connected",
|
||||
}
|
||||
|
||||
export type IPCOptions = {
|
||||
overrideTimeout?: number;
|
||||
};
|
||||
|
||||
export default class IPCService {
|
||||
// The current connection state of the socket.
|
||||
private connectionState: IPCConnectionState = IPCConnectionState.Disconnected;
|
||||
|
||||
// Messages that have been sent, but have not yet received responses
|
||||
private pendingMessages = new Map<string, Deferred<UnencryptedMessageResponse>>();
|
||||
|
||||
// A set of deferred promises that are awaiting socket connection
|
||||
private awaitingConnection = new Set<Deferred<void>>();
|
||||
|
||||
constructor(private socketName: string, private messageHandler: MessageHandler) {}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
console.log("[IPCService] connecting...");
|
||||
if (this.connectionState === IPCConnectionState.Connected) {
|
||||
// Socket is already connected. Don't throw, just allow the callsite to proceed
|
||||
return;
|
||||
}
|
||||
|
||||
const deferredConnections = new Deferred<void>();
|
||||
|
||||
this.awaitingConnection.add(deferredConnections);
|
||||
|
||||
// If the current connection state is disconnected, we should start trying to connect.
|
||||
// The only other possible connection state at this point is "connecting" and if this
|
||||
// is the case, we just want to add a deferred promise to the awaitingConnection collection
|
||||
// and not try to initiate the connection again.
|
||||
if (this.connectionState === IPCConnectionState.Disconnected) {
|
||||
this._connect();
|
||||
}
|
||||
|
||||
return deferredConnections.getPromise();
|
||||
}
|
||||
|
||||
private _connect() {
|
||||
this.connectionState = IPCConnectionState.Connecting;
|
||||
|
||||
NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => {
|
||||
// Process incoming message
|
||||
this.getSocket().on("message", (message: any) => {
|
||||
this.processMessage(message);
|
||||
});
|
||||
|
||||
this.getSocket().on("error", (error: Error) => {
|
||||
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
|
||||
// invoked multiple times each time a connection error happens
|
||||
console.log("[IPCService] errored");
|
||||
console.log(
|
||||
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m"
|
||||
);
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
console.log(`rejecting: ${deferred}`);
|
||||
deferred.reject(error);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
});
|
||||
|
||||
this.getSocket().on("connect", () => {
|
||||
console.log("[IPCService] connected");
|
||||
this.connectionState = IPCConnectionState.Connected;
|
||||
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
deferred.resolve(null);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
});
|
||||
|
||||
this.getSocket().on("disconnect", () => {
|
||||
console.log("[IPCService] disconnected");
|
||||
this.connectionState = IPCConnectionState.Disconnected;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
console.log("[IPCService] disconnecting...");
|
||||
if (this.connectionState !== IPCConnectionState.Disconnected) {
|
||||
NodeIPC.disconnect(this.socketName);
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(
|
||||
message: MessageCommon,
|
||||
options: IPCOptions = {}
|
||||
): Promise<UnencryptedMessageResponse> {
|
||||
console.log("[IPCService] sendMessage");
|
||||
if (this.pendingMessages.has(message.messageId)) {
|
||||
throw new Error(`A message with the id: ${message.messageId} has already been sent.`);
|
||||
}
|
||||
|
||||
// Creates a new deferred promise that allows us to convert a message received over the IPC socket
|
||||
// into a response for a message that we previously sent. This mechanism relies on the fact that we
|
||||
// create a unique message id and attach it with each message. Response messages are expected to
|
||||
// include the message id of the message they are responding to.
|
||||
const deferred = new Deferred<UnencryptedMessageResponse>();
|
||||
|
||||
this.pendingMessages.set(message.messageId, deferred);
|
||||
|
||||
this.getSocket().emit("message", message);
|
||||
|
||||
try {
|
||||
// Since we can not guarentee that a response message will ever be sent, we put a timeout
|
||||
// on messages
|
||||
return race({
|
||||
promise: deferred.getPromise(),
|
||||
timeout: options?.overrideTimeout ?? DEFAULT_MESSAGE_TIMEOUT,
|
||||
error: new Error(`Message: ${message.messageId} timed out`),
|
||||
});
|
||||
} catch (error) {
|
||||
// If there is a timeout, remove the message from the pending messages set
|
||||
// before triggering error handling elsewhere.
|
||||
this.pendingMessages.delete(message.messageId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private getSocket() {
|
||||
return NodeIPC.of[this.socketName];
|
||||
}
|
||||
|
||||
private processMessage(message: any) {
|
||||
// If the message is a response to a previous message, resolve the deferred promise that
|
||||
// is awaiting that response. Otherwise, assume this was a new message that wasn't sent as a
|
||||
// response and invoke the message handler.
|
||||
if (message.messageId && this.pendingMessages.has(message.messageId)) {
|
||||
const deferred = this.pendingMessages.get(message.messageId);
|
||||
|
||||
// In the future, this could be improved to add ability to reject, but most messages coming in are
|
||||
// encrypted at this point so we're unable to determine if they contain error info.
|
||||
deferred.resolve(message);
|
||||
|
||||
this.pendingMessages.delete(message.messageId);
|
||||
} else {
|
||||
this.messageHandler(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
apps/desktop/native-messaging-test-runner/src/logUtils.ts
Normal file
29
apps/desktop/native-messaging-test-runner/src/logUtils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
// Class for logging messages with colors for ease of reading important info
|
||||
// Reference: https://stackoverflow.com/a/41407246
|
||||
export class LogUtils {
|
||||
static logSuccess(message: string, payload?: any): void {
|
||||
this.logFormat(message, "32", payload);
|
||||
}
|
||||
|
||||
static logWarning(message: string, payload?: any): void {
|
||||
this.logFormat(message, "33", payload);
|
||||
}
|
||||
|
||||
static logError(message: string, payload?: any): void {
|
||||
this.logFormat(message, "31", payload);
|
||||
}
|
||||
|
||||
static logInfo(message: string, payload?: any): void {
|
||||
this.logFormat(message, "36", payload);
|
||||
}
|
||||
|
||||
private static logFormat(message: string, color: string, payload?: any) {
|
||||
if (payload) {
|
||||
console.log(`\x1b[${color}m ${message} \x1b[0m`, payload);
|
||||
} else {
|
||||
console.log(`\x1b[${color}m ${message} \x1b[0m`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import "module-alias/register";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
||||
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
|
||||
import { NodeCryptoFunctionService } from "@bitwarden/node/services/nodeCryptoFunction.service";
|
||||
|
||||
import { DecryptedCommandData } from "../../src/models/nativeMessaging/decryptedCommandData";
|
||||
import { EncryptedMessage } from "../../src/models/nativeMessaging/encryptedMessage";
|
||||
import { CredentialCreatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
|
||||
import { CredentialUpdatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
|
||||
import { EncryptedMessageResponse } from "../../src/models/nativeMessaging/encryptedMessageResponse";
|
||||
import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon";
|
||||
import { UnencryptedMessage } from "../../src/models/nativeMessaging/unencryptedMessage";
|
||||
import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse";
|
||||
|
||||
import IPCService, { IPCOptions } from "./ipcService";
|
||||
import * as config from "./variables";
|
||||
|
||||
type HandshakeResponse = {
|
||||
status: boolean;
|
||||
sharedKey: string;
|
||||
error?: "canceled" | "cannot-decrypt";
|
||||
};
|
||||
|
||||
const CONFIRMATION_MESSAGE_TIMEOUT = 100 * 1000; // 100 seconds
|
||||
|
||||
export default class NativeMessageService {
|
||||
private ipcService: IPCService;
|
||||
private nodeCryptoFunctionService: NodeCryptoFunctionService;
|
||||
private encryptService: EncryptService;
|
||||
|
||||
constructor(private apiVersion: number) {
|
||||
console.log("Starting native messaging service");
|
||||
this.ipcService = new IPCService(`bitwarden`, (rawMessage) => {
|
||||
console.log(`Received unexpected: `, rawMessage);
|
||||
});
|
||||
|
||||
this.nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
this.encryptService = new EncryptService(
|
||||
this.nodeCryptoFunctionService,
|
||||
new ConsoleLogService(false),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Commands
|
||||
|
||||
async sendHandshake(publicKey: string, applicationName: string): Promise<HandshakeResponse> {
|
||||
const rawResponse = await this.sendUnencryptedMessage(
|
||||
{
|
||||
command: "bw-handshake",
|
||||
payload: {
|
||||
publicKey,
|
||||
applicationName: applicationName,
|
||||
},
|
||||
},
|
||||
{
|
||||
overrideTimeout: CONFIRMATION_MESSAGE_TIMEOUT,
|
||||
}
|
||||
);
|
||||
return rawResponse.payload as HandshakeResponse;
|
||||
}
|
||||
|
||||
async checkStatus(key: string): Promise<DecryptedCommandData> {
|
||||
const encryptedCommand = await this.encryptCommandData(
|
||||
{
|
||||
command: "bw-status",
|
||||
},
|
||||
key
|
||||
);
|
||||
|
||||
const response = await this.sendEncryptedMessage({
|
||||
encryptedCommand,
|
||||
});
|
||||
|
||||
return this.decryptResponsePayload(response.encryptedPayload, key);
|
||||
}
|
||||
|
||||
async credentialRetrieval(key: string, uri: string): Promise<DecryptedCommandData> {
|
||||
const encryptedCommand = await this.encryptCommandData(
|
||||
{
|
||||
command: "bw-credential-retrieval",
|
||||
payload: {
|
||||
uri: uri,
|
||||
},
|
||||
},
|
||||
key
|
||||
);
|
||||
const response = await this.sendEncryptedMessage({
|
||||
encryptedCommand,
|
||||
});
|
||||
|
||||
return this.decryptResponsePayload(response.encryptedPayload, key);
|
||||
}
|
||||
|
||||
async credentialCreation(
|
||||
key: string,
|
||||
credentialData: CredentialCreatePayload
|
||||
): Promise<DecryptedCommandData> {
|
||||
const encryptedCommand = await this.encryptCommandData(
|
||||
{
|
||||
command: "bw-credential-create",
|
||||
payload: credentialData,
|
||||
},
|
||||
key
|
||||
);
|
||||
const response = await this.sendEncryptedMessage({
|
||||
encryptedCommand,
|
||||
});
|
||||
|
||||
return this.decryptResponsePayload(response.encryptedPayload, key);
|
||||
}
|
||||
|
||||
async credentialUpdate(
|
||||
key: string,
|
||||
credentialData: CredentialUpdatePayload
|
||||
): Promise<DecryptedCommandData> {
|
||||
const encryptedCommand = await this.encryptCommandData(
|
||||
{
|
||||
command: "bw-credential-update",
|
||||
payload: credentialData,
|
||||
},
|
||||
key
|
||||
);
|
||||
const response = await this.sendEncryptedMessage({
|
||||
encryptedCommand,
|
||||
});
|
||||
|
||||
return this.decryptResponsePayload(response.encryptedPayload, key);
|
||||
}
|
||||
|
||||
async generatePassword(key: string, userId: string): Promise<DecryptedCommandData> {
|
||||
const encryptedCommand = await this.encryptCommandData(
|
||||
{
|
||||
command: "bw-generate-password",
|
||||
payload: {
|
||||
userId: userId,
|
||||
},
|
||||
},
|
||||
key
|
||||
);
|
||||
const response = await this.sendEncryptedMessage({
|
||||
encryptedCommand,
|
||||
});
|
||||
|
||||
return this.decryptResponsePayload(response.encryptedPayload, key);
|
||||
}
|
||||
|
||||
// Private message sending
|
||||
|
||||
private async sendEncryptedMessage(
|
||||
message: Omit<EncryptedMessage, keyof MessageCommon>,
|
||||
options: IPCOptions = {}
|
||||
): Promise<EncryptedMessageResponse> {
|
||||
const result = await this.sendMessage(message, options);
|
||||
return result as EncryptedMessageResponse;
|
||||
}
|
||||
|
||||
private async sendUnencryptedMessage(
|
||||
message: Omit<UnencryptedMessage, keyof MessageCommon>,
|
||||
options: IPCOptions = {}
|
||||
): Promise<UnencryptedMessageResponse> {
|
||||
const result = await this.sendMessage(message, options);
|
||||
return result as UnencryptedMessageResponse;
|
||||
}
|
||||
|
||||
private async sendMessage(
|
||||
message:
|
||||
| Omit<UnencryptedMessage, keyof MessageCommon>
|
||||
| Omit<EncryptedMessage, keyof MessageCommon>,
|
||||
options: IPCOptions
|
||||
): Promise<EncryptedMessageResponse | UnencryptedMessageResponse> {
|
||||
// Attempt to connect before sending any messages. If the connection has already
|
||||
// been made, this is a NOOP within the IPCService.
|
||||
await this.ipcService.connect();
|
||||
|
||||
const commonFields: MessageCommon = {
|
||||
// Create a messageId that can be used as a lookup when we get a response
|
||||
messageId: uuidv4(),
|
||||
version: this.apiVersion,
|
||||
};
|
||||
const fullMessage: UnencryptedMessage | EncryptedMessage = {
|
||||
...message,
|
||||
...commonFields,
|
||||
};
|
||||
|
||||
console.log(`[NativeMessageService] sendMessage with id: ${fullMessage.messageId}`);
|
||||
|
||||
const response = await this.ipcService.sendMessage(fullMessage, options);
|
||||
|
||||
console.log(`[NativeMessageService] received response for message: ${fullMessage.messageId}`);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.ipcService.disconnect();
|
||||
}
|
||||
|
||||
// Data Encryption
|
||||
private async encryptCommandData(
|
||||
commandData: DecryptedCommandData,
|
||||
key: string
|
||||
): Promise<EncString> {
|
||||
const commandDataString = JSON.stringify(commandData);
|
||||
|
||||
const sharedKey = await this.getSharedKeyForKey(key);
|
||||
|
||||
return this.encryptService.encrypt(commandDataString, sharedKey);
|
||||
}
|
||||
|
||||
private async decryptResponsePayload(
|
||||
payload: EncString,
|
||||
key: string
|
||||
): Promise<DecryptedCommandData> {
|
||||
const sharedKey = await this.getSharedKeyForKey(key);
|
||||
const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey);
|
||||
|
||||
return JSON.parse(decrypted);
|
||||
}
|
||||
|
||||
private async getSharedKeyForKey(key: string): Promise<SymmetricCryptoKey> {
|
||||
const dataBuffer = Utils.fromB64ToArray(key).buffer;
|
||||
const privKey = Utils.fromB64ToArray(config.testRsaPrivateKey).buffer;
|
||||
|
||||
return new SymmetricCryptoKey(
|
||||
await this.nodeCryptoFunctionService.rsaDecrypt(dataBuffer, privKey, "sha1")
|
||||
);
|
||||
}
|
||||
}
|
||||
25
apps/desktop/native-messaging-test-runner/src/race.ts
Normal file
25
apps/desktop/native-messaging-test-runner/src/race.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const race = <T>({
|
||||
promise,
|
||||
timeout,
|
||||
error,
|
||||
}: {
|
||||
promise: Promise<T>;
|
||||
timeout: number;
|
||||
error?: Error;
|
||||
}) => {
|
||||
let timer = null;
|
||||
|
||||
// Similar to Promise.all, but instead of waiting for all, it resolves once one promise finishes.
|
||||
// Using this so we can reject if the timeout threshold is hit
|
||||
return Promise.race([
|
||||
new Promise<T>((_, reject) => {
|
||||
timer = setTimeout(reject, timeout, error);
|
||||
return timer;
|
||||
}),
|
||||
|
||||
promise.then((value) => {
|
||||
clearTimeout(timer);
|
||||
return value;
|
||||
}),
|
||||
]);
|
||||
};
|
||||
27
apps/desktop/native-messaging-test-runner/src/variables.ts
Normal file
27
apps/desktop/native-messaging-test-runner/src/variables.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const applicationName = "Native Messaging Test Runner";
|
||||
export const encryptionAlogrithm = "sha1";
|
||||
export const testRsaPublicKey =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" +
|
||||
"4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" +
|
||||
"RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN" +
|
||||
"084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc" +
|
||||
"xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB";
|
||||
export const testRsaPrivateKey =
|
||||
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz" +
|
||||
"YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L" +
|
||||
"nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/" +
|
||||
"YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK" +
|
||||
"PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q" +
|
||||
"Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj" +
|
||||
"WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh" +
|
||||
"5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk" +
|
||||
"1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU" +
|
||||
"BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf" +
|
||||
"TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU" +
|
||||
"q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv" +
|
||||
"q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX" +
|
||||
"5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1" +
|
||||
"eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE" +
|
||||
"Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8" +
|
||||
"+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ" +
|
||||
"BokBGnjFnTnKcs7nv/O8=";
|
||||
Reference in New Issue
Block a user