mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 02:03:39 +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,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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user