mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
Renamed folders: ./apps/desktop/src/models/nativeMessaging ./apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads ./apps/desktop/src/models/nativeMessaging/encryptedMessageResponses Renamed all files Cleaned up entries in whitelist-capital-letters.txt
This commit is contained in:
committed by
GitHub
parent
175eef5376
commit
db2d8aaa7e
253
apps/desktop/src/services/native-message-handler.service.ts
Normal file
253
apps/desktop/src/services/native-message-handler.service.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ipcRenderer } from "electron";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { StateService } from "@bitwarden/common/services/state.service";
|
||||
|
||||
import { DecryptedCommandData } from "../models/native-messaging/decrypted-command-data";
|
||||
import { EncryptedMessage } from "../models/native-messaging/encrypted-message";
|
||||
import { EncryptedMessageResponse } from "../models/native-messaging/encrypted-message-response";
|
||||
import { Message } from "../models/native-messaging/message";
|
||||
import { UnencryptedMessage } from "../models/native-messaging/unencrypted-message";
|
||||
import { UnencryptedMessageResponse } from "../models/native-messaging/unencrypted-message-response";
|
||||
|
||||
import { EncryptedMessageHandlerService } from "./encrypted-message-handler.service";
|
||||
|
||||
const EncryptionAlgorithm = "sha1";
|
||||
|
||||
@Injectable()
|
||||
export class NativeMessageHandlerService {
|
||||
private ddgSharedSecret: SymmetricCryptoKey;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private messagingService: MessagingService,
|
||||
private i18nService: I18nService,
|
||||
private encryptedMessageHandlerService: EncryptedMessageHandlerService
|
||||
) {}
|
||||
|
||||
async handleMessage(message: Message) {
|
||||
const decryptedCommand = message as UnencryptedMessage;
|
||||
if (message.version != NativeMessagingVersion.Latest) {
|
||||
this.sendResponse({
|
||||
messageId: message.messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "version-discrepancy",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (decryptedCommand.command === "bw-handshake") {
|
||||
await this.handleDecryptedMessage(decryptedCommand);
|
||||
} else {
|
||||
await this.handleEncryptedMessage(message as EncryptedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDecryptedMessage(message: UnencryptedMessage) {
|
||||
const { messageId, payload } = message;
|
||||
const { publicKey, applicationName } = payload;
|
||||
if (!publicKey) {
|
||||
this.sendResponse({
|
||||
messageId: messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "cannot-decrypt",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePublicKey = Utils.fromB64ToArray(publicKey).buffer;
|
||||
const ddgEnabled = await this.stateService.getEnableDuckDuckGoBrowserIntegration();
|
||||
|
||||
if (!ddgEnabled) {
|
||||
this.sendResponse({
|
||||
messageId: messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "canceled",
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for confirmation from user
|
||||
this.messagingService.send("setFocus");
|
||||
const submitted = await Swal.fire({
|
||||
heightAuto: false,
|
||||
titleText: this.i18nService.t("verifyNativeMessagingConnectionTitle", applicationName),
|
||||
html: `${this.i18nService.t("verifyNativeMessagingConnectionDesc")}<br>${this.i18nService.t(
|
||||
"verifyNativeMessagingConnectionWarning"
|
||||
)}`,
|
||||
showCancelButton: true,
|
||||
cancelButtonText: this.i18nService.t("no"),
|
||||
showConfirmButton: true,
|
||||
confirmButtonText: this.i18nService.t("yes"),
|
||||
allowOutsideClick: false,
|
||||
focusCancel: true,
|
||||
});
|
||||
|
||||
if (submitted.value !== true) {
|
||||
this.sendResponse({
|
||||
messageId: messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "canceled",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const secret = await this.cryptoFunctionService.randomBytes(64);
|
||||
this.ddgSharedSecret = new SymmetricCryptoKey(secret);
|
||||
const sharedKeyB64 = new SymmetricCryptoKey(secret).keyB64;
|
||||
|
||||
await this.stateService.setDuckDuckGoSharedKey(sharedKeyB64);
|
||||
|
||||
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
|
||||
secret,
|
||||
remotePublicKey,
|
||||
EncryptionAlgorithm
|
||||
);
|
||||
|
||||
this.sendResponse({
|
||||
messageId: messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
status: "success",
|
||||
sharedKey: Utils.fromBufferToB64(encryptedSecret),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.sendResponse({
|
||||
messageId: messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "cannot-decrypt",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleEncryptedMessage(message: EncryptedMessage) {
|
||||
message.encryptedCommand = EncString.fromJSON(message.encryptedCommand.toString());
|
||||
const decryptedCommandData = await this.decryptPayload(message);
|
||||
const { command } = decryptedCommandData;
|
||||
|
||||
try {
|
||||
const responseData = await this.encryptedMessageHandlerService.responseDataForCommand(
|
||||
decryptedCommandData
|
||||
);
|
||||
|
||||
await this.sendEncryptedResponse(message, { command, payload: responseData });
|
||||
} catch (error) {
|
||||
this.sendEncryptedResponse(message, { command, payload: {} });
|
||||
}
|
||||
}
|
||||
|
||||
private async encryptPayload(
|
||||
payload: DecryptedCommandData,
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<EncString> {
|
||||
return await this.cryptoService.encrypt(JSON.stringify(payload), key);
|
||||
}
|
||||
|
||||
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
|
||||
if (!this.ddgSharedSecret) {
|
||||
const storedKey = await this.stateService.getDuckDuckGoSharedKey();
|
||||
if (storedKey == null) {
|
||||
this.sendResponse({
|
||||
messageId: message.messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "cannot-decrypt",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.ddgSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
|
||||
}
|
||||
|
||||
try {
|
||||
let decryptedResult = await this.cryptoService.decryptToUtf8(
|
||||
message.encryptedCommand as EncString,
|
||||
this.ddgSharedSecret
|
||||
);
|
||||
|
||||
decryptedResult = this.trimNullCharsFromMessage(decryptedResult);
|
||||
|
||||
return JSON.parse(decryptedResult);
|
||||
} catch {
|
||||
this.sendResponse({
|
||||
messageId: message.messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "cannot-decrypt",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendEncryptedResponse(
|
||||
originalMessage: EncryptedMessage,
|
||||
response: DecryptedCommandData
|
||||
) {
|
||||
if (!this.ddgSharedSecret) {
|
||||
this.sendResponse({
|
||||
messageId: originalMessage.messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
payload: {
|
||||
error: "cannot-decrypt",
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedPayload = await this.encryptPayload(response, this.ddgSharedSecret);
|
||||
|
||||
this.sendResponse({
|
||||
messageId: originalMessage.messageId,
|
||||
version: NativeMessagingVersion.Latest,
|
||||
encryptedPayload,
|
||||
});
|
||||
}
|
||||
|
||||
private sendResponse(response: EncryptedMessageResponse | UnencryptedMessageResponse) {
|
||||
ipcRenderer.send("nativeMessagingReply", response);
|
||||
}
|
||||
|
||||
// Trim all null bytes padded at the end of messages. This happens with C encryption libraries.
|
||||
private trimNullCharsFromMessage(message: string): string {
|
||||
const charNull = 0;
|
||||
const charRightCurlyBrace = 125;
|
||||
const charRightBracket = 93;
|
||||
|
||||
for (let i = message.length - 1; i >= 0; i--) {
|
||||
if (message.charCodeAt(i) === charNull) {
|
||||
message = message.substring(0, message.length - 1);
|
||||
} else if (
|
||||
message.charCodeAt(i) === charRightCurlyBrace ||
|
||||
message.charCodeAt(i) === charRightBracket
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user