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:
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
5
apps/desktop/src/autofill/models/ssh-agent-setting.ts
Normal file
5
apps/desktop/src/autofill/models/ssh-agent-setting.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum SshAgentPromptType {
|
||||||
|
Always = "always",
|
||||||
|
Never = "never",
|
||||||
|
RememberUntilLock = "rememberUntilLock",
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user