2022-09-23 21:47:17 +02:00
|
|
|
import "module-alias/register";
|
|
|
|
|
|
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
|
|
|
|
import { Utils } from "@bitwarden/common/misc/utils";
|
2022-10-14 18:25:50 +02:00
|
|
|
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
|
|
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
2022-09-23 21:47:17 +02:00
|
|
|
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
2022-10-27 23:38:54 +02:00
|
|
|
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
|
2022-11-17 15:46:49 +01:00
|
|
|
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
|
2022-09-23 21:47:17 +02:00
|
|
|
|
2022-11-24 15:05:45 +01:00
|
|
|
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";
|
2022-09-23 21:47:17 +02:00
|
|
|
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;
|
2022-10-27 23:38:54 +02:00
|
|
|
private encryptService: EncryptServiceImplementation;
|
2022-09-23 21:47:17 +02:00
|
|
|
|
|
|
|
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();
|
2022-10-27 23:38:54 +02:00
|
|
|
this.encryptService = new EncryptServiceImplementation(
|
2022-09-23 21:47:17 +02:00
|
|
|
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")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|