1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00
Files
browser/apps/desktop/src/services/biometric-message-handler.service.spec.ts
Bernd Schoolmann ef20ca83b6 [PM-17121/17204] Fix fingerprint dialogs and disabled active biometric lock component (#12928)
* Fix biometrics unlock window being empty

* Add trust on sensitive action

* Add dialog for outdated desktop app and fix spelling

* Use updated fingerprint method

* Refactor connected app trust

* Move connected apps to ephemeral value store and show error on outdated browser

* Move trust logic to only occur when fingerprint setting is enabled

* Add more tests

* Simplify code

* Update ephemeral value list call to "listEphemeralValueKeys"

* Fix trust being ignored
2025-01-21 21:26:34 +01:00

391 lines
14 KiB
TypeScript

import { NgZone } from "@angular/core";
import { mock, MockProxy } from "jest-mock-extended";
import { of } 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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, I18nMockService } from "@bitwarden/components";
import {
KeyService,
BiometricsService,
BiometricStateService,
BiometricsCommands,
} from "@bitwarden/key-management";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { BiometricMessageHandlerService } from "./biometric-message-handler.service";
const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId;
const accounts = {
[SomeUser]: {
name: "some user",
email: "some.user@example.com",
emailVerified: true,
},
[AnotherUser]: {
name: "some other user",
email: "some.other.user@example.com",
emailVerified: true,
},
};
describe("BiometricMessageHandlerService", () => {
let service: BiometricMessageHandlerService;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let logService: MockProxy<LogService>;
let messagingService: MockProxy<MessagingService>;
let desktopSettingsService: DesktopSettingsService;
let biometricStateService: BiometricStateService;
let biometricsService: MockProxy<BiometricsService>;
let dialogService: MockProxy<DialogService>;
let accountService: AccountService;
let authService: MockProxy<AuthService>;
let ngZone: MockProxy<NgZone>;
let i18nService: MockProxy<I18nMockService>;
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
logService = mock<LogService>();
messagingService = mock<MessagingService>();
desktopSettingsService = mock<DesktopSettingsService>();
biometricStateService = mock<BiometricStateService>();
biometricsService = mock<BiometricsService>();
dialogService = mock<DialogService>();
accountService = new FakeAccountService(accounts);
authService = mock<AuthService>();
ngZone = mock<NgZone>();
i18nService = mock<I18nMockService>();
(global as any).ipc = {
platform: {
ephemeralStore: {
listEphemeralValueKeys: jest.fn(),
getEphemeralValue: jest.fn(),
removeEphemeralValue: jest.fn(),
setEphemeralValue: jest.fn(),
},
nativeMessaging: {
sendMessage: jest.fn(),
},
reloadProcess: jest.fn(),
},
};
cryptoFunctionService.rsaEncrypt.mockResolvedValue(Utils.fromUtf8ToArray("encrypted"));
cryptoFunctionService.randomBytes.mockResolvedValue(new Uint8Array(64) as CsprngArray);
service = new BiometricMessageHandlerService(
cryptoFunctionService,
keyService,
encryptService,
logService,
messagingService,
desktopSettingsService,
biometricStateService,
biometricsService,
dialogService,
accountService,
authService,
ngZone,
i18nService,
);
});
describe("setup encryption", () => {
it("should reject when user is not in app", async () => {
await service.handleMessage({
appId: "appId",
message: {
command: "setupEncryption",
messageId: 0,
userId: "unknownUser" as UserId,
},
});
expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
appId: "appId",
command: "wrongUserId",
});
});
it("should setup secure communication", async () => {
(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: null,
trusted: false,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: "setupEncryption",
messageId: 0,
userId: SomeUser,
publicKey: Utils.fromUtf8ToB64("publicKey"),
},
});
expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
appId: "appId",
command: "setupEncryption",
messageId: -1,
sharedSecret: Utils.fromUtf8ToB64("encrypted"),
});
expect((global as any).ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: false,
}),
);
});
it("should invalidate encryption if connection is not secured", async () => {
(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: null,
trusted: false,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: "biometricUnlock",
messageId: 0,
userId: SomeUser,
},
});
expect((global as any).ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
appId: "appId",
command: "invalidateEncryption",
});
});
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.decryptToUtf8.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([
"connectedApp_appId",
]);
(global as any).ipc.platform.ephemeralStore.getEphemeralValue.mockResolvedValue(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: false,
}),
);
ngZone.run.mockReturnValue({
closed: of(true),
});
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "verifyDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "verifiedDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: true,
}),
);
});
it("should send reject fingerprint when fingerprinting is required on modern unlock, and dialog is rejected, and it should not set to trusted", 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,
}),
);
ngZone.run.mockReturnValue({
closed: of(false),
});
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "verifyDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.nativeMessaging.sendMessage).toHaveBeenCalledWith({
command: "rejectedDesktopIPCFingerprint",
appId: "appId",
});
expect(ipc.platform.ephemeralStore.setEphemeralValue).not.toHaveBeenCalledWith(
"connectedApp_appId",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKey"),
sessionSecret: Utils.fromBufferToB64(new Uint8Array(64)),
trusted: true,
}),
);
});
it("should not attempt to verify when the connected app is already trusted", 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: true,
}),
);
encryptService.decryptToUtf8.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
}),
);
await service.handleMessage({
appId: "appId",
message: {
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
timestamp: Date.now(),
userId: SomeUser,
},
});
expect(ipc.platform.nativeMessaging.sendMessage).not.toHaveBeenCalledWith({
command: "verifyDesktopIPCFingerprint",
appId: "appId",
});
});
});
describe("process reload", () => {
const testCases = [
// don't reload when the active user is the requested one and unlocked
[SomeUser, AuthenticationStatus.Unlocked, SomeUser, false, false],
// do reload when the active user is the requested one but locked
[SomeUser, AuthenticationStatus.Locked, SomeUser, false, true],
// always reload when another user is active than the requested one
[SomeUser, AuthenticationStatus.Unlocked, AnotherUser, false, true],
[SomeUser, AuthenticationStatus.Locked, AnotherUser, false, true],
// don't reload in dev mode
[SomeUser, AuthenticationStatus.Unlocked, SomeUser, true, false],
[SomeUser, AuthenticationStatus.Locked, SomeUser, true, false],
[SomeUser, AuthenticationStatus.Unlocked, AnotherUser, true, false],
[SomeUser, AuthenticationStatus.Locked, AnotherUser, true, false],
];
it.each(testCases)(
"process reload for active user %s with auth status %s and other user %s and isdev: %s should process reload: %s",
async (activeUser, authStatus, messageUser, isDev, shouldReload) => {
await accountService.switchAccount(activeUser as UserId);
authService.authStatusFor$.mockReturnValue(of(authStatus as AuthenticationStatus));
(global as any).ipc.platform.isDev = isDev;
(global as any).ipc.platform.reloadProcess.mockClear();
await service.processReloadWhenRequired(messageUser as UserId);
if (shouldReload) {
expect((global as any).ipc.platform.reloadProcess).toHaveBeenCalled();
} else {
expect((global as any).ipc.platform.reloadProcess).not.toHaveBeenCalled();
}
},
);
});
});