Skip to content

Commit

Permalink
Add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
quexten committed Jan 20, 2025
1 parent af1025f commit 2677244
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 11 deletions.
241 changes: 234 additions & 7 deletions apps/desktop/src/services/biometric-message-handler.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, I18nMockService } from "@bitwarden/components";
import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
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 = {
Expand Down Expand Up @@ -72,6 +73,23 @@ describe("BiometricMessageHandlerService", () => {
ngZone = mock<NgZone>();
i18nService = mock<I18nMockService>();

(global as any).ipc = {
platform: {
ephemeralStore: {
listEphemeralValues: 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,
Expand All @@ -89,6 +107,215 @@ describe("BiometricMessageHandlerService", () => {
);
});

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.listEphemeralValues.mockResolvedValue(["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.listEphemeralValues.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.listEphemeralValues.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.listEphemeralValues.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.listEphemeralValues.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,
}),
);
});
});

describe("process reload", () => {
const testCases = [
// don't reload when the active user is the requested one and unlocked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,7 @@ export class BiometricMessageHandlerService {
// TODO: legacy, remove after 2025.3
case BiometricsCommands.Unlock: {
if (
(await firstValueFrom(
this.desktopSettingService.browserIntegrationFingerprintEnabled$,
)) &&
!(await this.connectedApps.get(appId)).trusted
await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)
) {
await this.send({ command: "biometricUnlock", response: "not available" }, appId);
await this.dialogService.openSimpleDialog({
Expand Down

0 comments on commit 2677244

Please sign in to comment.