mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[Pm-9823] Extract biometric messaging service (#10862)
This commit is contained in:
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -95,7 +95,8 @@ libs/common/src/autofill @bitwarden/team-autofill-dev
|
|||||||
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
||||||
# DuckDuckGo integration
|
# DuckDuckGo integration
|
||||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
||||||
apps/desktop/src/services/native-message-handler.service.ts @bitwarden/team-autofill-dev
|
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev
|
||||||
|
|
||||||
|
|
||||||
## Component Library ##
|
## Component Library ##
|
||||||
.storybook @bitwarden/team-design-system
|
.storybook @bitwarden/team-design-system
|
||||||
@@ -116,6 +117,7 @@ libs/key-management @bitwarden/team-key-management-dev
|
|||||||
apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev
|
apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev
|
||||||
apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev
|
apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev
|
||||||
apps/browser/src/background/nativeMessaging.background.ts @bitwarden/team-key-management-dev
|
apps/browser/src/background/nativeMessaging.background.ts @bitwarden/team-key-management-dev
|
||||||
|
apps/desktop/src/services/biometric-message-handler.service.ts @bitwarden/team-key-management-dev
|
||||||
|
|
||||||
## Locales ##
|
## Locales ##
|
||||||
apps/browser/src/_locales/en/messages.json
|
apps/browser/src/_locales/en/messages.json
|
||||||
|
|||||||
@@ -106,9 +106,10 @@ import { ElectronRendererStorageService } from "../../platform/services/electron
|
|||||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||||
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
||||||
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||||
|
import { BiometricMessageHandlerService } from "../../services/biometric-message-handler.service";
|
||||||
import { DesktopLockComponentService } from "../../services/desktop-lock-component.service";
|
import { DesktopLockComponentService } from "../../services/desktop-lock-component.service";
|
||||||
|
import { DuckDuckGoMessageHandlerService } from "../../services/duckduckgo-message-handler.service";
|
||||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||||
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
|
||||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||||
import { SearchBarService } from "../layout/search/search-bar.service";
|
import { SearchBarService } from "../layout/search/search-bar.service";
|
||||||
|
|
||||||
@@ -134,6 +135,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider(NativeMessagingService),
|
safeProvider(NativeMessagingService),
|
||||||
|
safeProvider(BiometricMessageHandlerService),
|
||||||
safeProvider(SearchBarService),
|
safeProvider(SearchBarService),
|
||||||
safeProvider(DialogService),
|
safeProvider(DialogService),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
@@ -257,7 +259,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: NativeMessageHandlerService,
|
provide: DuckDuckGoMessageHandlerService,
|
||||||
deps: [
|
deps: [
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
|
|||||||
238
apps/desktop/src/services/biometric-message-handler.service.ts
Normal file
238
apps/desktop/src/services/biometric-message-handler.service.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import { Injectable, NgZone } from "@angular/core";
|
||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
|
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 { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
|
||||||
|
import { LegacyMessage } from "../models/native-messaging/legacy-message";
|
||||||
|
import { LegacyMessageWrapper } from "../models/native-messaging/legacy-message-wrapper";
|
||||||
|
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||||
|
|
||||||
|
const MessageValidTimeout = 10 * 1000;
|
||||||
|
const HashAlgorithmForAsymmetricEncryption = "sha1";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BiometricMessageHandlerService {
|
||||||
|
constructor(
|
||||||
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
|
private keyService: KeyService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
|
private logService: LogService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private desktopSettingService: DesktopSettingsService,
|
||||||
|
private biometricStateService: BiometricStateService,
|
||||||
|
private biometricsService: BiometricsService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private ngZone: NgZone,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handleMessage(msg: LegacyMessageWrapper) {
|
||||||
|
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
|
||||||
|
|
||||||
|
// Request to setup secure encryption
|
||||||
|
if ("command" in rawMessage && rawMessage.command === "setupEncryption") {
|
||||||
|
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey);
|
||||||
|
|
||||||
|
// Validate the UserId to ensure we are logged into the same account.
|
||||||
|
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||||
|
const userIds = Object.keys(accounts);
|
||||||
|
if (!userIds.includes(rawMessage.userId)) {
|
||||||
|
ipc.platform.nativeMessaging.sendMessage({
|
||||||
|
command: "wrongUserId",
|
||||||
|
appId: appId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) {
|
||||||
|
ipc.platform.nativeMessaging.sendMessage({
|
||||||
|
command: "verifyFingerprint",
|
||||||
|
appId: appId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fingerprint = await this.keyService.getFingerprint(
|
||||||
|
rawMessage.userId,
|
||||||
|
remotePublicKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messagingService.send("setFocus");
|
||||||
|
|
||||||
|
const dialogRef = this.ngZone.run(() =>
|
||||||
|
BrowserSyncVerificationDialogComponent.open(this.dialogService, { fingerprint }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const browserSyncVerified = await firstValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (browserSyncVerified !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.secureCommunication(remotePublicKey, appId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) {
|
||||||
|
ipc.platform.nativeMessaging.sendMessage({
|
||||||
|
command: "invalidateEncryption",
|
||||||
|
appId: appId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: LegacyMessage = JSON.parse(
|
||||||
|
await this.encryptService.decryptToUtf8(
|
||||||
|
rawMessage as EncString,
|
||||||
|
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shared secret is invalidated, force re-authentication
|
||||||
|
if (message == null) {
|
||||||
|
ipc.platform.nativeMessaging.sendMessage({
|
||||||
|
command: "invalidateEncryption",
|
||||||
|
appId: appId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
|
||||||
|
this.logService.error("NativeMessage is to old, ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.command) {
|
||||||
|
case "biometricUnlock": {
|
||||||
|
const isTemporarilyDisabled =
|
||||||
|
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
|
||||||
|
!(await this.biometricsService.supportsBiometric());
|
||||||
|
if (isTemporarilyDisabled) {
|
||||||
|
return this.send({ command: "biometricUnlock", response: "not available" }, appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.biometricsService.supportsBiometric())) {
|
||||||
|
return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId =
|
||||||
|
(message.userId as UserId) ??
|
||||||
|
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const biometricUnlockPromise =
|
||||||
|
message.userId == null
|
||||||
|
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||||
|
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
||||||
|
if (!(await biometricUnlockPromise)) {
|
||||||
|
await this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
||||||
|
|
||||||
|
return this.ngZone.run(() =>
|
||||||
|
this.dialogService.openSimpleDialog({
|
||||||
|
type: "warning",
|
||||||
|
title: { key: "biometricsNotEnabledTitle" },
|
||||||
|
content: { key: "biometricsNotEnabledDesc" },
|
||||||
|
cancelButtonText: null,
|
||||||
|
acceptButtonText: { key: "cancel" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userKey = await this.keyService.getUserKeyFromStorage(
|
||||||
|
KeySuffixOptions.Biometric,
|
||||||
|
message.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userKey != null) {
|
||||||
|
await this.send(
|
||||||
|
{
|
||||||
|
command: "biometricUnlock",
|
||||||
|
response: "unlocked",
|
||||||
|
userKeyB64: userKey.keyB64,
|
||||||
|
},
|
||||||
|
appId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentlyActiveAccountId = (
|
||||||
|
await firstValueFrom(this.accountService.activeAccount$)
|
||||||
|
).id;
|
||||||
|
const isCurrentlyActiveAccountUnlocked =
|
||||||
|
(await this.authService.getAuthStatus(userId)) == AuthenticationStatus.Unlocked;
|
||||||
|
|
||||||
|
// prevent proc reloading an active account, when it is the same as the browser
|
||||||
|
if (currentlyActiveAccountId != message.userId || !isCurrentlyActiveAccountUnlocked) {
|
||||||
|
await ipc.platform.reloadProcess();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "biometricUnlockAvailable": {
|
||||||
|
const isAvailable = await this.biometricsService.supportsBiometric();
|
||||||
|
return this.send(
|
||||||
|
{
|
||||||
|
command: "biometricUnlockAvailable",
|
||||||
|
response: isAvailable ? "available" : "not available",
|
||||||
|
},
|
||||||
|
appId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
this.logService.error("NativeMessage, got unknown command: " + message.command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async send(message: any, appId: string) {
|
||||||
|
message.timestamp = Date.now();
|
||||||
|
|
||||||
|
const encrypted = await this.encryptService.encrypt(
|
||||||
|
JSON.stringify(message),
|
||||||
|
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
ipc.platform.nativeMessaging.sendMessage({ appId: appId, message: encrypted });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async secureCommunication(remotePublicKey: Uint8Array, appId: string) {
|
||||||
|
const secret = await this.cryptoFunctionService.randomBytes(64);
|
||||||
|
await ipc.platform.ephemeralStore.setEphemeralValue(
|
||||||
|
appId,
|
||||||
|
new SymmetricCryptoKey(secret).keyB64,
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
|
||||||
|
secret,
|
||||||
|
remotePublicKey,
|
||||||
|
HashAlgorithmForAsymmetricEncryption,
|
||||||
|
);
|
||||||
|
ipc.platform.nativeMessaging.sendMessage({
|
||||||
|
appId: appId,
|
||||||
|
command: "setupEncryption",
|
||||||
|
sharedSecret: Utils.fromBufferToB64(encryptedSecret),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,8 +26,8 @@ const HashAlgorithmForAsymmetricEncryption = "sha1";
|
|||||||
|
|
||||||
// This service handles messages using the protocol created for the DuckDuckGo integration.
|
// This service handles messages using the protocol created for the DuckDuckGo integration.
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NativeMessageHandlerService {
|
export class DuckDuckGoMessageHandlerService {
|
||||||
private ddgSharedSecret: SymmetricCryptoKey;
|
private duckduckgoSharedSecret: SymmetricCryptoKey;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
@@ -109,7 +109,7 @@ export class NativeMessageHandlerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secret = await this.cryptoFunctionService.randomBytes(64);
|
const secret = await this.cryptoFunctionService.randomBytes(64);
|
||||||
this.ddgSharedSecret = new SymmetricCryptoKey(secret);
|
this.duckduckgoSharedSecret = new SymmetricCryptoKey(secret);
|
||||||
const sharedKeyB64 = new SymmetricCryptoKey(secret).keyB64;
|
const sharedKeyB64 = new SymmetricCryptoKey(secret).keyB64;
|
||||||
|
|
||||||
await this.stateService.setDuckDuckGoSharedKey(sharedKeyB64);
|
await this.stateService.setDuckDuckGoSharedKey(sharedKeyB64);
|
||||||
@@ -166,7 +166,7 @@ export class NativeMessageHandlerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
|
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
|
||||||
if (!this.ddgSharedSecret) {
|
if (!this.duckduckgoSharedSecret) {
|
||||||
const storedKey = await this.stateService.getDuckDuckGoSharedKey();
|
const storedKey = await this.stateService.getDuckDuckGoSharedKey();
|
||||||
if (storedKey == null) {
|
if (storedKey == null) {
|
||||||
this.sendResponse({
|
this.sendResponse({
|
||||||
@@ -178,13 +178,13 @@ export class NativeMessageHandlerService {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.ddgSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
|
this.duckduckgoSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let decryptedResult = await this.encryptService.decryptToUtf8(
|
let decryptedResult = await this.encryptService.decryptToUtf8(
|
||||||
message.encryptedCommand as EncString,
|
message.encryptedCommand as EncString,
|
||||||
this.ddgSharedSecret,
|
this.duckduckgoSharedSecret,
|
||||||
"ddg-shared-key",
|
"ddg-shared-key",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ export class NativeMessageHandlerService {
|
|||||||
originalMessage: EncryptedMessage,
|
originalMessage: EncryptedMessage,
|
||||||
response: DecryptedCommandData,
|
response: DecryptedCommandData,
|
||||||
) {
|
) {
|
||||||
if (!this.ddgSharedSecret) {
|
if (!this.duckduckgoSharedSecret) {
|
||||||
this.sendResponse({
|
this.sendResponse({
|
||||||
messageId: originalMessage.messageId,
|
messageId: originalMessage.messageId,
|
||||||
version: NativeMessagingVersion.Latest,
|
version: NativeMessagingVersion.Latest,
|
||||||
@@ -219,7 +219,7 @@ export class NativeMessageHandlerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedPayload = await this.encryptPayload(response, this.ddgSharedSecret);
|
const encryptedPayload = await this.encryptPayload(response, this.duckduckgoSharedSecret);
|
||||||
|
|
||||||
this.sendResponse({
|
this.sendResponse({
|
||||||
messageId: originalMessage.messageId,
|
messageId: originalMessage.messageId,
|
||||||
@@ -1,48 +1,16 @@
|
|||||||
import { Injectable, NgZone } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
|
||||||
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 { UserId } from "@bitwarden/common/types/guid";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
|
||||||
import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
|
|
||||||
import { LegacyMessage } from "../models/native-messaging/legacy-message";
|
|
||||||
import { LegacyMessageWrapper } from "../models/native-messaging/legacy-message-wrapper";
|
import { LegacyMessageWrapper } from "../models/native-messaging/legacy-message-wrapper";
|
||||||
import { Message } from "../models/native-messaging/message";
|
import { Message } from "../models/native-messaging/message";
|
||||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
|
||||||
|
|
||||||
import { NativeMessageHandlerService } from "./native-message-handler.service";
|
import { BiometricMessageHandlerService } from "./biometric-message-handler.service";
|
||||||
|
import { DuckDuckGoMessageHandlerService } from "./duckduckgo-message-handler.service";
|
||||||
const MessageValidTimeout = 10 * 1000;
|
|
||||||
const HashAlgorithmForAsymmetricEncryption = "sha1";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NativeMessagingService {
|
export class NativeMessagingService {
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoFunctionService: CryptoFunctionService,
|
private duckduckgoMessageHandler: DuckDuckGoMessageHandlerService,
|
||||||
private keyService: KeyService,
|
private biometricMessageHandler: BiometricMessageHandlerService,
|
||||||
private encryptService: EncryptService,
|
|
||||||
private logService: LogService,
|
|
||||||
private messagingService: MessagingService,
|
|
||||||
private desktopSettingService: DesktopSettingsService,
|
|
||||||
private biometricStateService: BiometricStateService,
|
|
||||||
private biometricsService: BiometricsService,
|
|
||||||
private nativeMessageHandler: NativeMessageHandlerService,
|
|
||||||
private dialogService: DialogService,
|
|
||||||
private accountService: AccountService,
|
|
||||||
private authService: AuthService,
|
|
||||||
private ngZone: NgZone,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -53,202 +21,11 @@ export class NativeMessagingService {
|
|||||||
const outerMessage = msg as Message;
|
const outerMessage = msg as Message;
|
||||||
if (outerMessage.version) {
|
if (outerMessage.version) {
|
||||||
// If there is a version, it is a using the protocol created for the DuckDuckGo integration
|
// If there is a version, it is a using the protocol created for the DuckDuckGo integration
|
||||||
await this.nativeMessageHandler.handleMessage(outerMessage);
|
await this.duckduckgoMessageHandler.handleMessage(outerMessage);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await this.biometricMessageHandler.handleMessage(msg as LegacyMessageWrapper);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
|
|
||||||
|
|
||||||
// Request to setup secure encryption
|
|
||||||
if ("command" in rawMessage && rawMessage.command === "setupEncryption") {
|
|
||||||
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey);
|
|
||||||
|
|
||||||
// Validate the UserId to ensure we are logged into the same account.
|
|
||||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
|
||||||
const userIds = Object.keys(accounts);
|
|
||||||
if (!userIds.includes(rawMessage.userId)) {
|
|
||||||
ipc.platform.nativeMessaging.sendMessage({
|
|
||||||
command: "wrongUserId",
|
|
||||||
appId: appId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)) {
|
|
||||||
ipc.platform.nativeMessaging.sendMessage({
|
|
||||||
command: "verifyFingerprint",
|
|
||||||
appId: appId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fingerprint = await this.keyService.getFingerprint(
|
|
||||||
rawMessage.userId,
|
|
||||||
remotePublicKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.messagingService.send("setFocus");
|
|
||||||
|
|
||||||
const dialogRef = this.ngZone.run(() =>
|
|
||||||
BrowserSyncVerificationDialogComponent.open(this.dialogService, { fingerprint }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const browserSyncVerified = await firstValueFrom(dialogRef.closed);
|
|
||||||
|
|
||||||
if (browserSyncVerified !== true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.secureCommunication(remotePublicKey, appId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) {
|
|
||||||
ipc.platform.nativeMessaging.sendMessage({
|
|
||||||
command: "invalidateEncryption",
|
|
||||||
appId: appId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message: LegacyMessage = JSON.parse(
|
|
||||||
await this.encryptService.decryptToUtf8(
|
|
||||||
rawMessage as EncString,
|
|
||||||
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
|
|
||||||
`native-messaging-session-${appId}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Shared secret is invalidated, force re-authentication
|
|
||||||
if (message == null) {
|
|
||||||
ipc.platform.nativeMessaging.sendMessage({
|
|
||||||
command: "invalidateEncryption",
|
|
||||||
appId: appId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
|
|
||||||
this.logService.error("NativeMessage is to old, ignoring.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (message.command) {
|
|
||||||
case "biometricUnlock": {
|
|
||||||
const isTemporarilyDisabled =
|
|
||||||
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
|
|
||||||
!(await this.biometricsService.supportsBiometric());
|
|
||||||
if (isTemporarilyDisabled) {
|
|
||||||
return this.send({ command: "biometricUnlock", response: "not available" }, appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await this.biometricsService.supportsBiometric())) {
|
|
||||||
return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId =
|
|
||||||
(message.userId as UserId) ??
|
|
||||||
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
|
||||||
|
|
||||||
if (userId == null) {
|
|
||||||
return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const biometricUnlockPromise =
|
|
||||||
message.userId == null
|
|
||||||
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
|
||||||
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
|
||||||
if (!(await biometricUnlockPromise)) {
|
|
||||||
await this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
|
||||||
|
|
||||||
return this.ngZone.run(() =>
|
|
||||||
this.dialogService.openSimpleDialog({
|
|
||||||
type: "warning",
|
|
||||||
title: { key: "biometricsNotEnabledTitle" },
|
|
||||||
content: { key: "biometricsNotEnabledDesc" },
|
|
||||||
cancelButtonText: null,
|
|
||||||
acceptButtonText: { key: "cancel" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userKey = await this.keyService.getUserKeyFromStorage(
|
|
||||||
KeySuffixOptions.Biometric,
|
|
||||||
message.userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userKey != null) {
|
|
||||||
await this.send(
|
|
||||||
{
|
|
||||||
command: "biometricUnlock",
|
|
||||||
response: "unlocked",
|
|
||||||
userKeyB64: userKey.keyB64,
|
|
||||||
},
|
|
||||||
appId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentlyActiveAccountId = (
|
|
||||||
await firstValueFrom(this.accountService.activeAccount$)
|
|
||||||
).id;
|
|
||||||
const isCurrentlyActiveAccountUnlocked =
|
|
||||||
(await this.authService.getAuthStatus(userId)) == AuthenticationStatus.Unlocked;
|
|
||||||
|
|
||||||
// prevent proc reloading an active account, when it is the same as the browser
|
|
||||||
if (currentlyActiveAccountId != message.userId || !isCurrentlyActiveAccountUnlocked) {
|
|
||||||
await ipc.platform.reloadProcess();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "biometricUnlockAvailable": {
|
|
||||||
const isAvailable = await this.biometricsService.supportsBiometric();
|
|
||||||
return this.send(
|
|
||||||
{
|
|
||||||
command: "biometricUnlockAvailable",
|
|
||||||
response: isAvailable ? "available" : "not available",
|
|
||||||
},
|
|
||||||
appId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
this.logService.error("NativeMessage, got unknown command: " + message.command);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async send(message: any, appId: string) {
|
|
||||||
message.timestamp = Date.now();
|
|
||||||
|
|
||||||
const encrypted = await this.encryptService.encrypt(
|
|
||||||
JSON.stringify(message),
|
|
||||||
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
|
|
||||||
);
|
|
||||||
|
|
||||||
ipc.platform.nativeMessaging.sendMessage({ appId: appId, message: encrypted });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async secureCommunication(remotePublicKey: Uint8Array, appId: string) {
|
|
||||||
const secret = await this.cryptoFunctionService.randomBytes(64);
|
|
||||||
await ipc.platform.ephemeralStore.setEphemeralValue(
|
|
||||||
appId,
|
|
||||||
new SymmetricCryptoKey(secret).keyB64,
|
|
||||||
);
|
|
||||||
|
|
||||||
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
|
|
||||||
secret,
|
|
||||||
remotePublicKey,
|
|
||||||
HashAlgorithmForAsymmetricEncryption,
|
|
||||||
);
|
|
||||||
ipc.platform.nativeMessaging.sendMessage({
|
|
||||||
appId: appId,
|
|
||||||
command: "setupEncryption",
|
|
||||||
sharedSecret: Utils.fromBufferToB64(encryptedSecret),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user