1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +00:00

Browser integration verification always re-prompts after desktop app is locked (#14370)

This commit is contained in:
Maciej Zieniuk
2025-05-05 16:10:04 +02:00
committed by GitHub
parent a1e975a6ae
commit 9c8fc80971
2 changed files with 146 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
import { NgZone } from "@angular/core";
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { BehaviorSubject, filter, firstValueFrom, of, take, timeout, timer } from "rxjs";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -73,10 +73,13 @@ describe("BiometricMessageHandlerService", () => {
ngZone = mock<NgZone>();
i18nService = mock<I18nMockService>();
desktopSettingsService.browserIntegrationEnabled$ = of(false);
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(false);
(global as any).ipc = {
platform: {
ephemeralStore: {
listEphemeralValueKeys: jest.fn(),
listEphemeralValueKeys: jest.fn(() => Promise.resolve([])),
getEphemeralValue: jest.fn(),
removeEphemeralValue: jest.fn(),
setEphemeralValue: jest.fn(),
@@ -107,6 +110,129 @@ describe("BiometricMessageHandlerService", () => {
);
});
describe("constructor", () => {
let browserIntegrationEnabled = new BehaviorSubject<boolean>(true);
let browserIntegrationFingerprintEnabled = new BehaviorSubject<boolean>(true);
beforeEach(async () => {
(global as any).ipc = {
platform: {
ephemeralStore: {
listEphemeralValueKeys: jest.fn(() =>
Promise.resolve(["connectedApp_appId1", "connectedApp_appId2"]),
),
getEphemeralValue: jest.fn((key) => {
if (key === "connectedApp_appId1") {
return Promise.resolve(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKeyApp1"),
sessionSecret: Utils.fromUtf8ToB64("sessionSecretApp1"),
trusted: true,
}),
);
} else if (key === "connectedApp_appId2") {
return Promise.resolve(
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKeyApp2"),
sessionSecret: Utils.fromUtf8ToB64("sessionSecretApp2"),
trusted: false,
}),
);
}
return Promise.resolve(null);
}),
removeEphemeralValue: jest.fn(),
setEphemeralValue: jest.fn(),
},
},
};
desktopSettingsService.browserIntegrationEnabled$ = browserIntegrationEnabled.asObservable();
desktopSettingsService.browserIntegrationFingerprintEnabled$ =
browserIntegrationFingerprintEnabled.asObservable();
service = new BiometricMessageHandlerService(
cryptoFunctionService,
keyService,
encryptService,
logService,
messagingService,
desktopSettingsService,
biometricStateService,
biometricsService,
dialogService,
accountService,
authService,
ngZone,
i18nService,
);
});
afterEach(() => {
browserIntegrationEnabled = new BehaviorSubject<boolean>(true);
browserIntegrationFingerprintEnabled = new BehaviorSubject<boolean>(true);
desktopSettingsService.browserIntegrationEnabled$ = browserIntegrationEnabled.asObservable();
desktopSettingsService.browserIntegrationFingerprintEnabled$ =
browserIntegrationFingerprintEnabled.asObservable();
});
it("should clear connected apps when browser integration disabled", async () => {
browserIntegrationEnabled.next(false);
await firstValueFrom(
timer(0, 100).pipe(
filter(
() =>
(global as any).ipc.platform.ephemeralStore.removeEphemeralValue.mock.calls.length ==
2,
),
take(1),
timeout(1000),
),
);
expect((global as any).ipc.platform.ephemeralStore.removeEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId1",
);
expect((global as any).ipc.platform.ephemeralStore.removeEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId2",
);
});
it("should un-trust connected apps when browser integration verification fingerprint disabled", async () => {
browserIntegrationFingerprintEnabled.next(false);
await firstValueFrom(
timer(0, 100).pipe(
filter(
() =>
(global as any).ipc.platform.ephemeralStore.setEphemeralValue.mock.calls.length == 2,
),
take(1),
timeout(1000),
),
);
expect((global as any).ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId1",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKeyApp1"),
sessionSecret: Utils.fromUtf8ToB64("sessionSecretApp1"),
trusted: false,
}),
);
expect((global as any).ipc.platform.ephemeralStore.setEphemeralValue).toHaveBeenCalledWith(
"connectedApp_appId2",
JSON.stringify({
publicKey: Utils.fromUtf8ToB64("publicKeyApp2"),
sessionSecret: Utils.fromUtf8ToB64("sessionSecretApp2"),
trusted: false,
}),
);
});
});
describe("setup encryption", () => {
it("should ignore when public key missing in message", async () => {
await service.handleMessage({

View File

@@ -91,12 +91,27 @@ export class BiometricMessageHandlerService {
private i18nService: I18nService,
) {
combineLatest([
this.desktopSettingService.browserIntegrationFingerprintEnabled$,
this.desktopSettingService.browserIntegrationEnabled$,
this.desktopSettingService.browserIntegrationFingerprintEnabled$,
])
.pipe(
concatMap(async () => {
await this.connectedApps.clear();
concatMap(async ([browserIntegrationEnabled, browserIntegrationFingerprintEnabled]) => {
if (!browserIntegrationEnabled) {
this.logService.info("[Native Messaging IPC] Clearing connected apps");
await this.connectedApps.clear();
} else if (!browserIntegrationFingerprintEnabled) {
this.logService.info(
"[Native Messaging IPC] Browser integration fingerprint validation is disabled, untrusting all connected apps",
);
const connected = await this.connectedApps.list();
for (const appId of connected) {
const connectedApp = await this.connectedApps.get(appId);
if (connectedApp != null) {
connectedApp.trusted = false;
await this.connectedApps.set(appId, connectedApp);
}
}
}
}),
)
.subscribe();