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

[BEEEP/PM-15871] Add SSH-agent "never" and "remember until lock" reprompt settings (#13995)

* Add remember ssh authorizations setting

* Fix tests

* Fix authorization setting

* More detailed setting

* Add default value

* Cleanup
This commit is contained in:
Bernd Schoolmann
2025-05-05 23:09:27 +02:00
committed by GitHub
parent 669762a7f8
commit 961be9ed6a
7 changed files with 128 additions and 13 deletions

View File

@@ -440,6 +440,22 @@
"enableSshAgentDesc" | i18n "enableSshAgentDesc" | i18n
}}</small> }}</small>
</div> </div>
<div class="form-group" *ngIf="this.form.value.enableSshAgent">
<label for="sshAgentPromptBehavior">{{ "sshAgentPromptBehavior" | i18n }}</label>
<select
id="sshAgentPromptBehavior"
aria-describedby="sshAgentPromptBehaviorHelp"
formControlName="sshAgentPromptBehavior"
(change)="saveSshAgentPromptBehavior()"
>
<option *ngFor="let o of sshAgentPromptBehaviorOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
<small id="sshAgentPromptBehaviorHelp" class="help-block">{{
"sshAgentPromptBehaviorDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="!isLinux"> <div class="form-group" *ngIf="!isLinux">
<div class="checkbox"> <div class="checkbox">
<label for="allowScreenshots"> <label for="allowScreenshots">

View File

@@ -33,6 +33,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management"; import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management";
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service"; import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@@ -139,6 +140,7 @@ describe("SettingsComponent", () => {
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(false); desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(false);
desktopSettingsService.hardwareAcceleration$ = of(false); desktopSettingsService.hardwareAcceleration$ = of(false);
desktopSettingsService.sshAgentEnabled$ = of(false); desktopSettingsService.sshAgentEnabled$ = of(false);
desktopSettingsService.sshAgentPromptBehavior$ = of(SshAgentPromptType.Always);
desktopSettingsService.preventScreenshots$ = of(false); desktopSettingsService.preventScreenshots$ = of(false);
domainSettingsService.showFavicons$ = of(false); domainSettingsService.showFavicons$ = of(false);
desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$ = of(false); desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$ = of(false);

View File

@@ -45,6 +45,7 @@ import { DialogService } from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SetPinComponent } from "../../auth/components/set-pin.component";
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service"; import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@@ -63,6 +64,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
localeOptions: any[]; localeOptions: any[];
themeOptions: any[]; themeOptions: any[];
clearClipboardOptions: any[]; clearClipboardOptions: any[];
sshAgentPromptBehaviorOptions: any[];
supportsBiometric: boolean; supportsBiometric: boolean;
private timerId: any; private timerId: any;
showAlwaysShowDock = false; showAlwaysShowDock = false;
@@ -126,6 +128,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
}), }),
enableHardwareAcceleration: true, enableHardwareAcceleration: true,
enableSshAgent: false, enableSshAgent: false,
sshAgentPromptBehavior: SshAgentPromptType.Always,
allowScreenshots: false, allowScreenshots: false,
enableDuckDuckGoBrowserIntegration: false, enableDuckDuckGoBrowserIntegration: false,
theme: [null as Theme | null], theme: [null as Theme | null],
@@ -212,6 +215,17 @@ export class SettingsComponent implements OnInit, OnDestroy {
{ name: this.i18nService.t("twoMinutes"), value: 120 }, { name: this.i18nService.t("twoMinutes"), value: 120 },
{ name: this.i18nService.t("fiveMinutes"), value: 300 }, { name: this.i18nService.t("fiveMinutes"), value: 300 },
]; ];
this.sshAgentPromptBehaviorOptions = [
{
name: this.i18nService.t("sshAgentPromptBehaviorAlways"),
value: SshAgentPromptType.Always,
},
{ name: this.i18nService.t("sshAgentPromptBehaviorNever"), value: SshAgentPromptType.Never },
{
name: this.i18nService.t("sshAgentPromptBehaviorRememberUntilLock"),
value: SshAgentPromptType.RememberUntilLock,
},
];
} }
async ngOnInit() { async ngOnInit() {
@@ -312,6 +326,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.desktopSettingsService.hardwareAcceleration$, this.desktopSettingsService.hardwareAcceleration$,
), ),
enableSshAgent: await firstValueFrom(this.desktopSettingsService.sshAgentEnabled$), enableSshAgent: await firstValueFrom(this.desktopSettingsService.sshAgentEnabled$),
sshAgentPromptBehavior: await firstValueFrom(
this.desktopSettingsService.sshAgentPromptBehavior$,
),
allowScreenshots: !(await firstValueFrom(this.desktopSettingsService.preventScreenshots$)), 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$),
@@ -779,10 +796,15 @@ export class SettingsComponent implements OnInit, OnDestroy {
} }
async saveSshAgent() { async saveSshAgent() {
this.logService.debug("Saving Ssh Agent settings", this.form.value.enableSshAgent);
await this.desktopSettingsService.setSshAgentEnabled(this.form.value.enableSshAgent); await this.desktopSettingsService.setSshAgentEnabled(this.form.value.enableSshAgent);
} }
async saveSshAgentPromptBehavior() {
await this.desktopSettingsService.setSshAgentPromptBehavior(
this.form.value.sshAgentPromptBehavior,
);
}
async savePreventScreenshots() { async savePreventScreenshots() {
await this.desktopSettingsService.setPreventScreenshots(!this.form.value.allowScreenshots); await this.desktopSettingsService.setPreventScreenshots(!this.form.value.allowScreenshots);

View File

@@ -0,0 +1,5 @@
export enum SshAgentPromptType {
Always = "always",
Never = "never",
RememberUntilLock = "rememberUntilLock",
}

View File

@@ -34,6 +34,7 @@ import { DialogService, ToastService } from "@bitwarden/components";
import { ApproveSshRequestComponent } from "../../platform/components/approve-ssh-request"; import { ApproveSshRequestComponent } from "../../platform/components/approve-ssh-request";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { SshAgentPromptType } from "../models/ssh-agent-setting";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
@@ -43,6 +44,8 @@ export class SshAgentService implements OnDestroy {
SSH_VAULT_UNLOCK_REQUEST_TIMEOUT = 60_000; SSH_VAULT_UNLOCK_REQUEST_TIMEOUT = 60_000;
SSH_REQUEST_UNLOCK_POLLING_INTERVAL = 100; SSH_REQUEST_UNLOCK_POLLING_INTERVAL = 100;
private authorizedSshKeys: Record<string, Date> = {};
private isFeatureFlagEnabled = false; private isFeatureFlagEnabled = false;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@@ -166,19 +169,26 @@ export class SshAgentService implements OnDestroy {
.catch((e) => this.logService.error("Failed to respond to SSH request", e)); .catch((e) => this.logService.error("Failed to respond to SSH request", e));
} }
const cipher = ciphers.find((cipher) => cipher.id == cipherId); if (await this.needsAuthorization(cipherId, isAgentForwarding)) {
ipc.platform.focusWindow();
const cipher = ciphers.find((cipher) => cipher.id == cipherId);
const dialogRef = ApproveSshRequestComponent.open(
this.dialogService,
cipher.name,
application,
isAgentForwarding,
namespace,
);
ipc.platform.focusWindow(); if (await firstValueFrom(dialogRef.closed)) {
const dialogRef = ApproveSshRequestComponent.open( await this.rememberAuthorization(cipherId);
this.dialogService, return ipc.platform.sshAgent.signRequestResponse(requestId, true);
cipher.name, } else {
application, return ipc.platform.sshAgent.signRequestResponse(requestId, false);
isAgentForwarding, }
namespace, } else {
); return ipc.platform.sshAgent.signRequestResponse(requestId, true);
}
const result = await firstValueFrom(dialogRef.closed);
return ipc.platform.sshAgent.signRequestResponse(requestId, result);
}), }),
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
@@ -190,6 +200,7 @@ export class SshAgentService implements OnDestroy {
return; return;
} }
this.authorizedSshKeys = {};
this.logService.info("Active account changed, clearing SSH keys"); this.logService.info("Active account changed, clearing SSH keys");
ipc.platform.sshAgent ipc.platform.sshAgent
.clearKeys() .clearKeys()
@@ -211,6 +222,7 @@ export class SshAgentService implements OnDestroy {
} }
this.logService.info("Active account observable completed, clearing SSH keys"); this.logService.info("Active account observable completed, clearing SSH keys");
this.authorizedSshKeys = {};
ipc.platform.sshAgent ipc.platform.sshAgent
.clearKeys() .clearKeys()
.catch((e) => this.logService.error("Failed to clear SSH keys", e)); .catch((e) => this.logService.error("Failed to clear SSH keys", e));
@@ -270,4 +282,25 @@ export class SshAgentService implements OnDestroy {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
private async rememberAuthorization(cipherId: string): Promise<void> {
this.authorizedSshKeys[cipherId] = new Date();
}
private async needsAuthorization(cipherId: string, isForward: boolean): Promise<boolean> {
// Agent forwarding ALWAYS needs authorization because it is a remote machine
if (isForward) {
return true;
}
const promptType = await firstValueFrom(this.desktopSettingsService.sshAgentPromptBehavior$);
switch (promptType) {
case SshAgentPromptType.Never:
return false;
case SshAgentPromptType.Always:
return true;
case SshAgentPromptType.RememberUntilLock:
return !(cipherId in this.authorizedSshKeys);
}
}
} }

View File

@@ -237,6 +237,24 @@
"enableSshAgentHelp": { "enableSshAgentHelp": {
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault."
}, },
"sshAgentPromptBehavior": {
"message": "Ask for authorization when using SSH agent"
},
"sshAgentPromptBehaviorDesc": {
"message": "Choose how to handle SSH-agent authorization requests."
},
"sshAgentPromptBehaviorHelp": {
"message": "Remember SSH authorizations"
},
"sshAgentPromptBehaviorAlways": {
"message": "Always"
},
"sshAgentPromptBehaviorNever": {
"message": "Never"
},
"sshAgentPromptBehaviorRememberUntilLock": {
"message": "Remember until vault is locked"
},
"premiumRequired": { "premiumRequired": {
"message": "Premium required" "message": "Premium required"
}, },

View File

@@ -8,6 +8,7 @@ import {
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
import { ModalModeState, WindowState } from "../models/domain/window-state"; import { ModalModeState, WindowState } from "../models/domain/window-state";
export const HARDWARE_ACCELERATION = new KeyDefinition<boolean>( export const HARDWARE_ACCELERATION = new KeyDefinition<boolean>(
@@ -70,6 +71,15 @@ const SSH_AGENT_ENABLED = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "ssh
deserializer: (b) => b, deserializer: (b) => b,
}); });
const SSH_AGENT_PROMPT_BEHAVIOR = new UserKeyDefinition<SshAgentPromptType>(
DESKTOP_SETTINGS_DISK,
"sshAgentRememberAuthorizations",
{
deserializer: (b) => b,
clearOn: [],
},
);
const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "minimizeOnCopy", { const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "minimizeOnCopy", {
deserializer: (b) => b, deserializer: (b) => b,
clearOn: [], // User setting, no need to clear clearOn: [], // User setting, no need to clear
@@ -159,6 +169,11 @@ export class DesktopSettingsService {
sshAgentEnabled$ = this.sshAgentEnabledState.state$.pipe(map(Boolean)); sshAgentEnabled$ = this.sshAgentEnabledState.state$.pipe(map(Boolean));
private readonly sshAgentPromptBehavior = this.stateProvider.getActive(SSH_AGENT_PROMPT_BEHAVIOR);
sshAgentPromptBehavior$ = this.sshAgentPromptBehavior.state$.pipe(
map((v) => v ?? SshAgentPromptType.Always),
);
private readonly preventScreenshotState = this.stateProvider.getGlobal(PREVENT_SCREENSHOTS); private readonly preventScreenshotState = this.stateProvider.getGlobal(PREVENT_SCREENSHOTS);
/** /**
@@ -292,6 +307,10 @@ export class DesktopSettingsService {
await this.sshAgentEnabledState.update(() => value); await this.sshAgentEnabledState.update(() => value);
} }
async setSshAgentPromptBehavior(value: SshAgentPromptType) {
await this.sshAgentPromptBehavior.update(() => value);
}
/** /**
* Sets the minimize on copy value for the current user. * Sets the minimize on copy value for the current user.
* @param value `true` if the application should minimize when a value is copied, * @param value `true` if the application should minimize when a value is copied,