mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 22:44:11 +00:00
* [PM-10395] Add new item type ssh key (#10360) * Implement ssh-key cipher type * Fix linting * Fix edit and view components for ssh-keys on desktop * Fix tests * Remove ssh key type references * Remove add ssh key option * Fix typo * Add tests * [PM-10399] Add ssh key import export for bitwarden json (#10529) * Add ssh key import export for bitwarden json * Remove key type from ssh key export * [PM-10406] Add privatekey publickey and fingerprint to both add-edit and view co… (#11046) * Add privatekey publickey and fingerprint to both add-edit and view components * Remove wrong a11y title * Fix testid * [PM-10098] SSH Agent & SSH Key creation for Bitwarden Desktop (#10293) * Add ssh agent, generator & import * Move ssh agent code to bitwarden-russh crate * Remove generator component * Cleanup * Cleanup * Remove left over sshGenerator reference * Cleanup * Add documentation to sshkeyimportstatus * Fix outdated variable name * Update apps/desktop/src/platform/preload.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Rename renderersshagent * Rename MainSshAgentService * Improve clarity of 'id' variables being used * Improve clarity of 'id' variables being used * Update apps/desktop/src/vault/app/vault/add-edit.component.html Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Fix outdated cipher/messageid names * Rename SSH to Ssh * Make agent syncing more reactive * Move constants to top of class * Make sshkey cipher filtering clearer * Add stricter equality check on ssh key unlock * Fix build and messages * Fix incorrect featureflag name * Replace anonymous async function with switchmap pipe * Fix build * Update apps/desktop/desktop_native/napi/src/lib.rs Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Revert incorrectly renamed 'Ssh' usages to SSH * Run cargo fmt * Clean up ssh agent sock path logic * Cleanup and split to platform specific files * Small cleanup * Pull out generator and importer into core * Rename renderersshagentservice to sshagentservice * Rename cipheruuid to cipher_id * Drop ssh dependencies from napi crate * Clean up windows build * Small cleanup * Small cleanup * Cleanup * Add rxjs pipeline for agent services * [PM-12555] Pkcs8 sshkey import & general ssh key import tests (#11048) * Add pkcs8 import and tests * Add key type unsupported error * Remove unsupported formats * Remove code for unsupported formats * Fix encrypted pkcs8 import * Add ed25519 pkcs8 unencrypted test file * SSH agent rxjs tweaks (#11148) * feat: rewrite sshagent.signrequest as purely observable * feat: fail the request when unlock times out * chore: clean up, add some clarifying comments * chore: remove unused dependency * fix: result `undefined` crashing in NAPI -> Rust * Allow concurrent SSH requests in rust * Remove unwraps * Cleanup and add init service init call * Fix windows * Fix timeout behavior on locked vault --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Fix libc dependency being duplicated * fix SSH casing (#11840) * Move ssh agent behind feature flag (#11841) * Move ssh agent behind feature flag * Add separate flag for ssh agent * [PM-14215] fix unsupported key type error message (#11788) * Fix error message for import of unsupported ssh keys * Use triple equals in add-edit component for ssh keys --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> Co-authored-by: aj-bw <81774843+aj-bw@users.noreply.github.com>
184 lines
6.5 KiB
TypeScript
184 lines
6.5 KiB
TypeScript
import { Injectable, OnDestroy } from "@angular/core";
|
|
import {
|
|
catchError,
|
|
combineLatest,
|
|
concatMap,
|
|
EMPTY,
|
|
filter,
|
|
from,
|
|
map,
|
|
of,
|
|
Subject,
|
|
switchMap,
|
|
takeUntil,
|
|
timeout,
|
|
TimeoutError,
|
|
timer,
|
|
withLatestFrom,
|
|
} from "rxjs";
|
|
|
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
|
import { DialogService, ToastService } from "@bitwarden/components";
|
|
|
|
import { ApproveSshRequestComponent } from "../components/approve-ssh-request";
|
|
|
|
import { DesktopSettingsService } from "./desktop-settings.service";
|
|
|
|
@Injectable({
|
|
providedIn: "root",
|
|
})
|
|
export class SshAgentService implements OnDestroy {
|
|
SSH_REFRESH_INTERVAL = 1000;
|
|
SSH_VAULT_UNLOCK_REQUEST_TIMEOUT = 1000 * 60;
|
|
SSH_REQUEST_UNLOCK_POLLING_INTERVAL = 100;
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
constructor(
|
|
private cipherService: CipherService,
|
|
private logService: LogService,
|
|
private dialogService: DialogService,
|
|
private messageListener: MessageListener,
|
|
private authService: AuthService,
|
|
private toastService: ToastService,
|
|
private i18nService: I18nService,
|
|
private desktopSettingsService: DesktopSettingsService,
|
|
private configService: ConfigService,
|
|
) {}
|
|
|
|
async init() {
|
|
const isSshAgentFeatureEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHAgent);
|
|
if (isSshAgentFeatureEnabled) {
|
|
await ipc.platform.sshAgent.init();
|
|
|
|
this.messageListener
|
|
.messages$(new CommandDefinition("sshagent.signrequest"))
|
|
.pipe(
|
|
withLatestFrom(this.authService.activeAccountStatus$),
|
|
// This switchMap handles unlocking the vault if it is locked:
|
|
// - If the vault is locked, we will wait for it to be unlocked.
|
|
// - If the vault is not unlocked within the timeout, we will abort the flow.
|
|
// - If the vault is unlocked, we will continue with the flow.
|
|
// switchMap is used here to prevent multiple requests from being processed at the same time,
|
|
// and will cancel the previous request if a new one is received.
|
|
switchMap(([message, status]) => {
|
|
if (status !== AuthenticationStatus.Unlocked) {
|
|
ipc.platform.focusWindow();
|
|
this.toastService.showToast({
|
|
variant: "info",
|
|
title: null,
|
|
message: this.i18nService.t("sshAgentUnlockRequired"),
|
|
});
|
|
return this.authService.activeAccountStatus$.pipe(
|
|
filter((status) => status === AuthenticationStatus.Unlocked),
|
|
timeout(this.SSH_VAULT_UNLOCK_REQUEST_TIMEOUT),
|
|
catchError((error: unknown) => {
|
|
if (error instanceof TimeoutError) {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: null,
|
|
message: this.i18nService.t("sshAgentUnlockTimeout"),
|
|
});
|
|
const requestId = message.requestId as number;
|
|
// Abort flow by sending a false response.
|
|
// Returning an empty observable this will prevent the rest of the flow from executing
|
|
return from(ipc.platform.sshAgent.signRequestResponse(requestId, false)).pipe(
|
|
map(() => EMPTY),
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}),
|
|
map(() => message),
|
|
);
|
|
}
|
|
|
|
return of(message);
|
|
}),
|
|
// This switchMap handles fetching the ciphers from the vault.
|
|
switchMap((message) =>
|
|
from(this.cipherService.getAllDecrypted()).pipe(
|
|
map((ciphers) => [message, ciphers] as const),
|
|
),
|
|
),
|
|
// This concatMap handles showing the dialog to approve the request.
|
|
concatMap(([message, decryptedCiphers]) => {
|
|
const cipherId = message.cipherId as string;
|
|
const requestId = message.requestId as number;
|
|
|
|
if (decryptedCiphers === undefined) {
|
|
return of(false).pipe(
|
|
switchMap((result) =>
|
|
ipc.platform.sshAgent.signRequestResponse(requestId, Boolean(result)),
|
|
),
|
|
);
|
|
}
|
|
|
|
const cipher = decryptedCiphers.find((cipher) => cipher.id == cipherId);
|
|
|
|
ipc.platform.focusWindow();
|
|
const dialogRef = ApproveSshRequestComponent.open(
|
|
this.dialogService,
|
|
cipher.name,
|
|
this.i18nService.t("unknownApplication"),
|
|
);
|
|
|
|
return dialogRef.closed.pipe(
|
|
switchMap((result) => {
|
|
return ipc.platform.sshAgent.signRequestResponse(requestId, Boolean(result));
|
|
}),
|
|
);
|
|
}),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe();
|
|
|
|
combineLatest([
|
|
timer(0, this.SSH_REFRESH_INTERVAL),
|
|
this.desktopSettingsService.sshAgentEnabled$,
|
|
])
|
|
.pipe(
|
|
concatMap(async ([, enabled]) => {
|
|
if (!enabled) {
|
|
await ipc.platform.sshAgent.setKeys([]);
|
|
return;
|
|
}
|
|
|
|
const ciphers = await this.cipherService.getAllDecrypted();
|
|
if (ciphers == null) {
|
|
await ipc.platform.sshAgent.lock();
|
|
return;
|
|
}
|
|
|
|
const sshCiphers = ciphers.filter(
|
|
(cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted,
|
|
);
|
|
const keys = sshCiphers.map((cipher) => {
|
|
return {
|
|
name: cipher.name,
|
|
privateKey: cipher.sshKey.privateKey,
|
|
cipherId: cipher.id,
|
|
};
|
|
});
|
|
await ipc.platform.sshAgent.setKeys(keys);
|
|
}),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe();
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
}
|