1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-14006] Prevent screenshot setting V2 (#12570)

* Add screenshot protection to windows and mac

* Update messaging of screencapture prevention feature

* Rename settings key

* Default allow screenshots

* Update screenshot setting description

* Fix typo

* Add confirm visible prompt
This commit is contained in:
Bernd Schoolmann
2025-02-10 20:02:13 +01:00
committed by GitHub
parent 2b5c7861e2
commit 543cf0fb3f
5 changed files with 106 additions and 1 deletions

View File

@@ -436,6 +436,23 @@
"enableSshAgentDesc" | i18n "enableSshAgentDesc" | i18n
}}</small> }}</small>
</div> </div>
<div class="form-group" *ngIf="!isLinux">
<div class="checkbox">
<label for="allowScreenshots">
<input
id="allowScreenshots"
type="checkbox"
aria-describedby="allowScreenshotsHelp"
formControlName="allowScreenshots"
(change)="savePreventScreenshots()"
/>
{{ "allowScreenshots" | i18n }}
</label>
</div>
<small id="allowScreenshotsHelp" class="help-block">{{
"allowScreenshotsDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption"> <div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
<div class="checkbox"> <div class="checkbox">
<label for="enableDuckDuckGoBrowserIntegration"> <label for="enableDuckDuckGoBrowserIntegration">

View File

@@ -3,7 +3,16 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs"; import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators"; import {
concatMap,
debounceTime,
filter,
map,
switchMap,
takeUntil,
tap,
timeout,
} from "rxjs/operators";
import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@@ -116,6 +125,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
}), }),
enableHardwareAcceleration: true, enableHardwareAcceleration: true,
enableSshAgent: false, enableSshAgent: false,
allowScreenshots: false,
enableDuckDuckGoBrowserIntegration: false, enableDuckDuckGoBrowserIntegration: false,
theme: [null as ThemeType | null], theme: [null as ThemeType | null],
locale: [null as string | null], locale: [null as string | null],
@@ -287,6 +297,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.desktopSettingsService.hardwareAcceleration$, this.desktopSettingsService.hardwareAcceleration$,
), ),
enableSshAgent: await firstValueFrom(this.desktopSettingsService.sshAgentEnabled$), enableSshAgent: await firstValueFrom(this.desktopSettingsService.sshAgentEnabled$),
allowScreenshots: !(await firstValueFrom(this.desktopSettingsService.preventScreenshots$)),
theme: await firstValueFrom(this.themeStateService.selectedTheme$), theme: await firstValueFrom(this.themeStateService.selectedTheme$),
locale: await firstValueFrom(this.i18nService.userSetLocale$), locale: await firstValueFrom(this.i18nService.userSetLocale$),
}; };
@@ -769,6 +780,33 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.desktopSettingsService.setSshAgentEnabled(this.form.value.enableSshAgent); await this.desktopSettingsService.setSshAgentEnabled(this.form.value.enableSshAgent);
} }
async savePreventScreenshots() {
await this.desktopSettingsService.setPreventScreenshots(!this.form.value.allowScreenshots);
if (!this.form.value.allowScreenshots) {
const dialogRef = this.dialogService.openSimpleDialogRef({
title: { key: "confirmWindowStillVisibleTitle" },
content: { key: "confirmWindowStillVisibleContent" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "info",
});
let enabled = true;
try {
enabled = await firstValueFrom(dialogRef.closed.pipe(timeout(10000)));
} catch {
enabled = false;
} finally {
dialogRef.close();
}
if (!enabled) {
await this.desktopSettingsService.setPreventScreenshots(false);
this.form.controls.allowScreenshots.setValue(true, { emitEvent: false });
}
}
}
private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> { private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
let vaultTimeoutOptions: VaultTimeoutOption[] = [ let vaultTimeoutOptions: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 }, { name: this.i18nService.t("oneMinute"), value: 1 },

View File

@@ -3496,6 +3496,12 @@
"changeAcctEmail": { "changeAcctEmail": {
"message": "Change account email" "message": "Change account email"
}, },
"allowScreenshots": {
"message": "Allow screen capture"
},
"allowScreenshotsDesc": {
"message": "Allow the Bitwarden desktop application to be captured in screenshots and viewed in remote desktop sessions. Disabling this will prevent access on some external displays."
},
"organizationUpgradeRequired": { "organizationUpgradeRequired": {
"message": "Organization upgrade required" "message": "Organization upgrade required"
}, },
@@ -3505,6 +3511,12 @@
"upgradeOrganizationDesc": { "upgradeOrganizationDesc": {
"message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
}, },
"confirmWindowStillVisibleTitle": {
"message": "Confirm window still visible"
},
"confirmWindowStillVisibleContent": {
"message": "Please confirm that the window is still visible."
},
"updateBrowserOrDisableFingerprintDialogTitle": { "updateBrowserOrDisableFingerprintDialogTitle": {
"message": "Extension update required" "message": "Extension update required"
}, },

View File

@@ -76,6 +76,13 @@ export class WindowMain {
} }
}); });
this.desktopSettingsService.preventScreenshots$.subscribe((prevent) => {
if (this.win == null) {
return;
}
this.win.setContentProtection(prevent);
});
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
try { try {
if (!isMacAppStore()) { if (!isMacAppStore()) {
@@ -277,6 +284,14 @@ export class WindowMain {
}); });
}); });
firstValueFrom(this.desktopSettingsService.preventScreenshots$)
.then((preventScreenshots) => {
this.win.setContentProtection(preventScreenshots);
})
.catch((e) => {
this.logService.error(e);
});
if (this.createWindowCallback) { if (this.createWindowCallback) {
this.createWindowCallback(this.win); this.createWindowCallback(this.win);
} }

View File

@@ -75,6 +75,14 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "
clearOn: [], // User setting, no need to clear clearOn: [], // User setting, no need to clear
}); });
const PREVENT_SCREENSHOTS = new KeyDefinition<boolean>(
DESKTOP_SETTINGS_DISK,
"preventScreenshots",
{
deserializer: (b) => b,
},
);
/** /**
* Various settings for controlling application behavior specific to the desktop client. * Various settings for controlling application behavior specific to the desktop client.
*/ */
@@ -147,6 +155,13 @@ export class DesktopSettingsService {
sshAgentEnabled$ = this.sshAgentEnabledState.state$.pipe(map(Boolean)); sshAgentEnabled$ = this.sshAgentEnabledState.state$.pipe(map(Boolean));
private readonly preventScreenshotState = this.stateProvider.getGlobal(PREVENT_SCREENSHOTS);
/**
* The application setting for whether or not to allow screenshots of the app.
*/
preventScreenshots$ = this.preventScreenshotState.state$.pipe(map(Boolean));
private readonly minimizeOnCopyState = this.stateProvider.getActive(MINIMIZE_ON_COPY); private readonly minimizeOnCopyState = this.stateProvider.getActive(MINIMIZE_ON_COPY);
/** /**
@@ -270,4 +285,12 @@ export class DesktopSettingsService {
async setMinimizeOnCopy(value: boolean, userId: UserId) { async setMinimizeOnCopy(value: boolean, userId: UserId) {
await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value); await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value);
} }
/**
* Sets the setting for whether or not the screenshot protection is enabled.
* @param value `true` if the screenshot protection is enabled, `false` if it is not.
*/
async setPreventScreenshots(value: boolean) {
await this.preventScreenshotState.update(() => value);
}
} }