mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[BEEEP] Remove legacy biometrics protocol (#15004)
* Remove legacy biometrics protocol * Remove legacy message handling on desktop
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
@@ -11,7 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
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 { KeyService, BiometricStateService, BiometricsCommands } from "@bitwarden/key-management";
|
||||
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
|
||||
@@ -81,9 +81,6 @@ export class NativeMessagingBackground {
|
||||
|
||||
private messageId = 0;
|
||||
private callbacks = new Map<number, Callback>();
|
||||
|
||||
isConnectedToOutdatedDesktopClient = true;
|
||||
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
@@ -137,7 +134,6 @@ export class NativeMessagingBackground {
|
||||
// Safari has a bundled native component which is always available, no need to
|
||||
// check if the desktop app is running.
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
this.isConnectedToOutdatedDesktopClient = false;
|
||||
connectedCallback();
|
||||
}
|
||||
|
||||
@@ -189,14 +185,6 @@ export class NativeMessagingBackground {
|
||||
this.secureChannel.sharedSecret = new SymmetricCryptoKey(decrypted);
|
||||
this.logService.info("[Native Messaging IPC] Secure channel established");
|
||||
|
||||
if ("messageId" in message) {
|
||||
this.logService.info("[Native Messaging IPC] Non-legacy desktop client");
|
||||
this.isConnectedToOutdatedDesktopClient = false;
|
||||
} else {
|
||||
this.logService.info("[Native Messaging IPC] Legacy desktop client");
|
||||
this.isConnectedToOutdatedDesktopClient = true;
|
||||
}
|
||||
|
||||
this.secureChannel.setupResolve();
|
||||
break;
|
||||
}
|
||||
@@ -286,29 +274,6 @@ export class NativeMessagingBackground {
|
||||
async callCommand(message: Message): Promise<any> {
|
||||
const messageId = this.messageId++;
|
||||
|
||||
if (
|
||||
message.command == BiometricsCommands.Unlock ||
|
||||
message.command == BiometricsCommands.IsAvailable
|
||||
) {
|
||||
// TODO remove after 2025.3
|
||||
// wait until there is no other callbacks, or timeout
|
||||
const call = await firstValueFrom(
|
||||
race(
|
||||
from([false]).pipe(delay(5000)),
|
||||
timer(0, 100).pipe(
|
||||
filter(() => this.callbacks.size === 0),
|
||||
map(() => true),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!call) {
|
||||
this.logService.info(
|
||||
`[Native Messaging IPC] Message of type ${message.command} did not get a response before timing out`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const callback = new Promise((resolver, rejecter) => {
|
||||
this.callbacks.set(messageId, { resolver, rejecter });
|
||||
});
|
||||
@@ -417,22 +382,6 @@ export class NativeMessagingBackground {
|
||||
|
||||
const messageId = message.messageId;
|
||||
|
||||
if (
|
||||
message.command == BiometricsCommands.Unlock ||
|
||||
message.command == BiometricsCommands.IsAvailable
|
||||
) {
|
||||
this.logService.info(
|
||||
`[Native Messaging IPC] Received legacy message of type ${message.command}`,
|
||||
);
|
||||
const messageId: number | undefined = this.callbacks.keys().next().value;
|
||||
if (messageId != null) {
|
||||
const resolver = this.callbacks.get(messageId);
|
||||
this.callbacks.delete(messageId);
|
||||
resolver!.resolver(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.callbacks.has(messageId)) {
|
||||
const callback = this.callbacks!.get(messageId)!;
|
||||
this.callbacks.delete(messageId);
|
||||
|
||||
@@ -35,17 +35,10 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
|
||||
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.Unlock,
|
||||
});
|
||||
return response.response == "unlocked";
|
||||
} else {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.AuthenticateWithBiometrics,
|
||||
});
|
||||
return response.response;
|
||||
}
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.AuthenticateWithBiometrics,
|
||||
});
|
||||
return response.response;
|
||||
} catch (e) {
|
||||
this.logService.info("Biometric authentication failed", e);
|
||||
return false;
|
||||
@@ -60,23 +53,12 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
|
||||
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.IsAvailable,
|
||||
});
|
||||
const resp =
|
||||
response.response == "available"
|
||||
? BiometricsStatus.Available
|
||||
: BiometricsStatus.HardwareUnavailable;
|
||||
return resp;
|
||||
} else {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.GetBiometricsStatus,
|
||||
});
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.GetBiometricsStatus,
|
||||
});
|
||||
|
||||
if (response.response) {
|
||||
return response.response;
|
||||
}
|
||||
if (response.response) {
|
||||
return response.response;
|
||||
}
|
||||
return BiometricsStatus.Available;
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
@@ -90,43 +72,23 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
|
||||
// todo remove after 2025.3
|
||||
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.Unlock,
|
||||
});
|
||||
if (response.response == "unlocked") {
|
||||
const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
|
||||
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
|
||||
if (await this.keyService.validateUserKey(userKey, userId)) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
// to update badge and other things
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
return userKey;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.UnlockWithBiometricsForUser,
|
||||
userId: userId,
|
||||
});
|
||||
if (response.response) {
|
||||
// In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled
|
||||
const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
|
||||
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
|
||||
if (await this.keyService.validateUserKey(userKey, userId)) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
// to update badge and other things
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
return userKey;
|
||||
}
|
||||
} else {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.UnlockWithBiometricsForUser,
|
||||
userId: userId,
|
||||
});
|
||||
if (response.response) {
|
||||
// In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled
|
||||
const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
|
||||
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
|
||||
if (await this.keyService.validateUserKey(userKey, userId)) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
// to update badge and other things
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
return userKey;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.info("Biometric unlock for user failed", e);
|
||||
@@ -140,10 +102,6 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
|
||||
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
|
||||
return await this.getBiometricsStatus();
|
||||
}
|
||||
|
||||
return (
|
||||
await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.GetBiometricsStatusForUser,
|
||||
@@ -161,7 +119,7 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
private async ensureConnected() {
|
||||
if (!this.nativeMessagingBackground().connected) {
|
||||
await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.IsAvailable,
|
||||
command: BiometricsCommands.GetBiometricsStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,38 +335,6 @@ describe("BiometricMessageHandlerService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show update dialog when legacy unlock is requested with fingerprint active", async () => {
|
||||
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true);
|
||||
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
|
||||
"connectedApp_appId",
|
||||
]);
|
||||
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
publicKey: Utils.fromUtf8ToB64("publicKey"),
|
||||
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
|
||||
trusted: false,
|
||||
}),
|
||||
);
|
||||
encryptService.decryptString.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
command: "biometricUnlock",
|
||||
messageId: 0,
|
||||
timestamp: Date.now(),
|
||||
userId: SomeUser,
|
||||
}),
|
||||
);
|
||||
await service.handleMessage({
|
||||
appId: "appId",
|
||||
message: {
|
||||
command: "biometricUnlock",
|
||||
messageId: 0,
|
||||
timestamp: Date.now(),
|
||||
userId: SomeUser,
|
||||
},
|
||||
});
|
||||
expect(dialogService.openSimpleDialog).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send verify fingerprint when fingerprinting is required on modern unlock, and dialog is accepted, and set to trusted", async () => {
|
||||
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(true);
|
||||
(global as any).ipc.platform.ephemeralStore.listEphemeralValueKeys.mockResolvedValue([
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, NgZone } from "@angular/core";
|
||||
import { combineLatest, concatMap, firstValueFrom, map } from "rxjs";
|
||||
import { combineLatest, concatMap, firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
@@ -255,102 +255,6 @@ export class BiometricMessageHandlerService {
|
||||
appId,
|
||||
);
|
||||
}
|
||||
// TODO: legacy, remove after 2025.3
|
||||
case BiometricsCommands.IsAvailable: {
|
||||
const available =
|
||||
(await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available;
|
||||
return this.send(
|
||||
{
|
||||
command: BiometricsCommands.IsAvailable,
|
||||
response: available ? "available" : "not available",
|
||||
},
|
||||
appId,
|
||||
);
|
||||
}
|
||||
// TODO: legacy, remove after 2025.3
|
||||
case BiometricsCommands.Unlock: {
|
||||
if (
|
||||
await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)
|
||||
) {
|
||||
await this.send({ command: "biometricUnlock", response: "not available" }, appId);
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("updateBrowserOrDisableFingerprintDialogTitle"),
|
||||
content: this.i18nService.t("updateBrowserOrDisableFingerprintDialogMessage"),
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isTemporarilyDisabled =
|
||||
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
|
||||
!((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available);
|
||||
if (isTemporarilyDisabled) {
|
||||
return this.send({ command: "biometricUnlock", response: "not available" }, appId);
|
||||
}
|
||||
|
||||
if (!((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available)) {
|
||||
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 biometricUnlock =
|
||||
message.userId == null
|
||||
? await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||
: await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
||||
if (!biometricUnlock) {
|
||||
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.biometricsService.unlockWithBiometricsForUser(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) {
|
||||
ipc.platform.reloadProcess();
|
||||
}
|
||||
} else {
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.logService.error("NativeMessage, got unknown command: " + message.command);
|
||||
break;
|
||||
|
||||
@@ -12,8 +12,4 @@ export enum BiometricsCommands {
|
||||
|
||||
/** Checks whether the biometric unlock can be enabled. */
|
||||
CanEnableBiometricUnlock = "canEnableBiometricUnlock",
|
||||
|
||||
// legacy
|
||||
Unlock = "biometricUnlock",
|
||||
IsAvailable = "biometricUnlockAvailable",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user