1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-31 22:51:28 +01:00

[Pm-9823] Extract biometric messaging service (#10862)

This commit is contained in:
Bernd Schoolmann 2024-11-22 17:41:14 -08:00 committed by GitHub
parent 493f81a017
commit 341a93aa47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 262 additions and 243 deletions

4
.github/CODEOWNERS vendored
View File

@ -95,7 +95,8 @@ libs/common/src/autofill @bitwarden/team-autofill-dev
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
# DuckDuckGo integration
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 ##
.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/src/services/native-messaging.service.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 ##
apps/browser/src/_locales/en/messages.json

View File

@ -106,9 +106,10 @@ import { ElectronRendererStorageService } from "../../platform/services/electron
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
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 { DuckDuckGoMessageHandlerService } from "../../services/duckduckgo-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 { SearchBarService } from "../layout/search/search-bar.service";
@ -134,6 +135,7 @@ const safeProviders: SafeProvider[] = [
deps: [],
}),
safeProvider(NativeMessagingService),
safeProvider(BiometricMessageHandlerService),
safeProvider(SearchBarService),
safeProvider(DialogService),
safeProvider({
@ -257,7 +259,7 @@ const safeProviders: SafeProvider[] = [
],
}),
safeProvider({
provide: NativeMessageHandlerService,
provide: DuckDuckGoMessageHandlerService,
deps: [
StateServiceAbstraction,
EncryptService,

View 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),
});
}
}

View File

@ -26,8 +26,8 @@ const HashAlgorithmForAsymmetricEncryption = "sha1";
// This service handles messages using the protocol created for the DuckDuckGo integration.
@Injectable()
export class NativeMessageHandlerService {
private ddgSharedSecret: SymmetricCryptoKey;
export class DuckDuckGoMessageHandlerService {
private duckduckgoSharedSecret: SymmetricCryptoKey;
constructor(
private stateService: StateService,
@ -109,7 +109,7 @@ export class NativeMessageHandlerService {
}
const secret = await this.cryptoFunctionService.randomBytes(64);
this.ddgSharedSecret = new SymmetricCryptoKey(secret);
this.duckduckgoSharedSecret = new SymmetricCryptoKey(secret);
const sharedKeyB64 = new SymmetricCryptoKey(secret).keyB64;
await this.stateService.setDuckDuckGoSharedKey(sharedKeyB64);
@ -166,7 +166,7 @@ export class NativeMessageHandlerService {
}
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
if (!this.ddgSharedSecret) {
if (!this.duckduckgoSharedSecret) {
const storedKey = await this.stateService.getDuckDuckGoSharedKey();
if (storedKey == null) {
this.sendResponse({
@ -178,13 +178,13 @@ export class NativeMessageHandlerService {
});
return;
}
this.ddgSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
this.duckduckgoSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
}
try {
let decryptedResult = await this.encryptService.decryptToUtf8(
message.encryptedCommand as EncString,
this.ddgSharedSecret,
this.duckduckgoSharedSecret,
"ddg-shared-key",
);
@ -207,7 +207,7 @@ export class NativeMessageHandlerService {
originalMessage: EncryptedMessage,
response: DecryptedCommandData,
) {
if (!this.ddgSharedSecret) {
if (!this.duckduckgoSharedSecret) {
this.sendResponse({
messageId: originalMessage.messageId,
version: NativeMessagingVersion.Latest,
@ -219,7 +219,7 @@ export class NativeMessageHandlerService {
return;
}
const encryptedPayload = await this.encryptPayload(response, this.ddgSharedSecret);
const encryptedPayload = await this.encryptPayload(response, this.duckduckgoSharedSecret);
this.sendResponse({
messageId: originalMessage.messageId,

View File

@ -1,48 +1,16 @@
import { Injectable, NgZone } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { Injectable } from "@angular/core";
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 { Message } from "../models/native-messaging/message";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { NativeMessageHandlerService } from "./native-message-handler.service";
const MessageValidTimeout = 10 * 1000;
const HashAlgorithmForAsymmetricEncryption = "sha1";
import { BiometricMessageHandlerService } from "./biometric-message-handler.service";
import { DuckDuckGoMessageHandlerService } from "./duckduckgo-message-handler.service";
@Injectable()
export class NativeMessagingService {
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 nativeMessageHandler: NativeMessageHandlerService,
private dialogService: DialogService,
private accountService: AccountService,
private authService: AuthService,
private ngZone: NgZone,
private duckduckgoMessageHandler: DuckDuckGoMessageHandlerService,
private biometricMessageHandler: BiometricMessageHandlerService,
) {}
init() {
@ -53,202 +21,11 @@ export class NativeMessagingService {
const outerMessage = msg as Message;
if (outerMessage.version) {
// 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;
}
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),
});
}
}