mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 03:33:54 +00:00
[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
This commit is contained in:
@@ -9,21 +9,22 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c
|
||||
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 } from "@bitwarden/components";
|
||||
import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
|
||||
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";
|
||||
|
||||
(global as any).ipc = {
|
||||
platform: {
|
||||
reloadProcess: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const SomeUser = "SomeUser" as UserId;
|
||||
const AnotherUser = "SomeOtherUser" as UserId;
|
||||
const accounts = {
|
||||
@@ -54,6 +55,7 @@ describe("BiometricMessageHandlerService", () => {
|
||||
let accountService: AccountService;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let ngZone: MockProxy<NgZone>;
|
||||
let i18nService: MockProxy<I18nMockService>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
@@ -69,6 +71,24 @@ describe("BiometricMessageHandlerService", () => {
|
||||
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,
|
||||
@@ -83,9 +103,256 @@ describe("BiometricMessageHandlerService", () => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user