import "module-alias/register"; import { v4 as uuidv4 } from "uuid"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; import { DecryptedCommandData } from "../../src/models/native-messaging/decrypted-command-data"; import { EncryptedMessage } from "../../src/models/native-messaging/encrypted-message"; import { CredentialCreatePayload } from "../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload"; import { CredentialUpdatePayload } from "../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload"; import { EncryptedMessageResponse } from "../../src/models/native-messaging/encrypted-message-response"; import { MessageCommon } from "../../src/models/native-messaging/message-common"; import { UnencryptedMessage } from "../../src/models/native-messaging/unencrypted-message"; import { UnencryptedMessageResponse } from "../../src/models/native-messaging/unencrypted-message-response"; import IPCService, { IPCOptions } from "./ipc.service"; 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: EncryptServiceImplementation; 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 EncryptServiceImplementation( this.nodeCryptoFunctionService, new ConsoleLogService(false), false ); } // Commands async sendHandshake(publicKey: string, applicationName: string): Promise { 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 { 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 { 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 { 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 { 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 { 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, options: IPCOptions = {} ): Promise { const result = await this.sendMessage(message, options); return result as EncryptedMessageResponse; } private async sendUnencryptedMessage( message: Omit, options: IPCOptions = {} ): Promise { const result = await this.sendMessage(message, options); return result as UnencryptedMessageResponse; } private async sendMessage( message: | Omit | Omit, options: IPCOptions ): Promise { // 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 { 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 { const sharedKey = await this.getSharedKeyForKey(key); const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey); return JSON.parse(decrypted); } private async getSharedKeyForKey(key: string): Promise { const dataBuffer = Utils.fromB64ToArray(key).buffer; const privKey = Utils.fromB64ToArray(config.testRsaPrivateKey).buffer; return new SymmetricCryptoKey( await this.nodeCryptoFunctionService.rsaDecrypt(dataBuffer, privKey, "sha1") ); } }