mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 14:53:33 +00:00
[PM-16837] Fix agent only loading when featureflag is on during startup (#12742)
* Fix ssh generation and import not being available when agent feature-flag is disabled * Fix agent only loading when featureflag is on during startup
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { app, ipcMain } from "electron";
|
import { app } from "electron";
|
||||||
import { Subject, firstValueFrom } from "rxjs";
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
@@ -257,12 +257,7 @@ export class Main {
|
|||||||
this.clipboardMain = new ClipboardMain();
|
this.clipboardMain = new ClipboardMain();
|
||||||
this.clipboardMain.init();
|
this.clipboardMain.init();
|
||||||
|
|
||||||
ipcMain.handle("sshagent.init", async (event: any, message: any) => {
|
this.sshAgentService = new MainSshAgentService(this.logService, this.messagingService);
|
||||||
if (this.sshAgentService == null) {
|
|
||||||
this.sshAgentService = new MainSshAgentService(this.logService, this.messagingService);
|
|
||||||
this.sshAgentService.init();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new EphemeralValueStorageService();
|
new EphemeralValueStorageService();
|
||||||
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
|
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ export class MainSshAgentService {
|
|||||||
return sshagent.importKey(privateKey, password);
|
return sshagent.importKey(privateKey, password);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle("sshagent.init", async (event: any, message: any) => {
|
||||||
|
this.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("sshagent.isloaded", async (event: any) => {
|
||||||
|
return this.agentState != null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ const sshAgent = {
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
isLoaded(): Promise<boolean> {
|
||||||
|
return ipcRenderer.invoke("sshagent.isloaded");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const powermonitor = {
|
const powermonitor = {
|
||||||
|
|||||||
@@ -61,153 +61,87 @@ export class SshAgentService implements OnDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const isSshAgentFeatureEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHAgent);
|
this.configService
|
||||||
if (isSshAgentFeatureEnabled) {
|
.getFeatureFlag$(FeatureFlag.SSHAgent)
|
||||||
await ipc.platform.sshAgent.init();
|
.pipe(
|
||||||
|
concatMap(async (enabled) => {
|
||||||
|
if (enabled && !(await ipc.platform.sshAgent.isLoaded())) {
|
||||||
|
return this.initSshAgent();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
this.messageListener
|
private async initSshAgent() {
|
||||||
.messages$(new CommandDefinition("sshagent.signrequest"))
|
await ipc.platform.sshAgent.init();
|
||||||
.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({
|
|
||||||
first: 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(async ([message, ciphers]) => {
|
|
||||||
const cipherId = message.cipherId as string;
|
|
||||||
const isListRequest = message.isListRequest as boolean;
|
|
||||||
const requestId = message.requestId as number;
|
|
||||||
let application = message.processName as string;
|
|
||||||
if (application == "") {
|
|
||||||
application = this.i18nService.t("unknownApplication");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isListRequest) {
|
|
||||||
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);
|
|
||||||
await ipc.platform.sshAgent.signRequestResponse(requestId, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ciphers === undefined) {
|
|
||||||
ipc.platform.sshAgent
|
|
||||||
.signRequestResponse(requestId, false)
|
|
||||||
.catch((e) => this.logService.error("Failed to respond to SSH request", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
const cipher = ciphers.find((cipher) => cipher.id == cipherId);
|
|
||||||
|
|
||||||
|
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();
|
ipc.platform.focusWindow();
|
||||||
const dialogRef = ApproveSshRequestComponent.open(
|
this.toastService.showToast({
|
||||||
this.dialogService,
|
variant: "info",
|
||||||
cipher.name,
|
title: null,
|
||||||
application,
|
message: this.i18nService.t("sshAgentUnlockRequired"),
|
||||||
|
});
|
||||||
|
return this.authService.activeAccountStatus$.pipe(
|
||||||
|
filter((status) => status === AuthenticationStatus.Unlocked),
|
||||||
|
timeout({
|
||||||
|
first: 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),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await firstValueFrom(dialogRef.closed);
|
return of(message);
|
||||||
return ipc.platform.sshAgent.signRequestResponse(requestId, result);
|
}),
|
||||||
}),
|
// This switchMap handles fetching the ciphers from the vault.
|
||||||
takeUntil(this.destroy$),
|
switchMap((message) =>
|
||||||
)
|
from(this.cipherService.getAllDecrypted()).pipe(
|
||||||
.subscribe();
|
map((ciphers) => [message, ciphers] as const),
|
||||||
|
),
|
||||||
this.accountService.activeAccount$.pipe(skip(1), takeUntil(this.destroy$)).subscribe({
|
),
|
||||||
next: (account) => {
|
// This concatMap handles showing the dialog to approve the request.
|
||||||
this.logService.info("Active account changed, clearing SSH keys");
|
concatMap(async ([message, ciphers]) => {
|
||||||
ipc.platform.sshAgent
|
const cipherId = message.cipherId as string;
|
||||||
.clearKeys()
|
const isListRequest = message.isListRequest as boolean;
|
||||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
const requestId = message.requestId as number;
|
||||||
},
|
let application = message.processName as string;
|
||||||
error: (e: unknown) => {
|
if (application == "") {
|
||||||
this.logService.error("Error in active account observable", e);
|
application = this.i18nService.t("unknownApplication");
|
||||||
ipc.platform.sshAgent
|
}
|
||||||
.clearKeys()
|
|
||||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
this.logService.info("Active account observable completed, clearing SSH keys");
|
|
||||||
ipc.platform.sshAgent
|
|
||||||
.clearKeys()
|
|
||||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
combineLatest([
|
|
||||||
timer(0, this.SSH_REFRESH_INTERVAL),
|
|
||||||
this.desktopSettingsService.sshAgentEnabled$,
|
|
||||||
])
|
|
||||||
.pipe(
|
|
||||||
concatMap(async ([, enabled]) => {
|
|
||||||
if (!enabled) {
|
|
||||||
await ipc.platform.sshAgent.clearKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ciphers = await this.cipherService.getAllDecrypted();
|
|
||||||
if (ciphers == null) {
|
|
||||||
await ipc.platform.sshAgent.lock();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isListRequest) {
|
||||||
const sshCiphers = ciphers.filter(
|
const sshCiphers = ciphers.filter(
|
||||||
(cipher) =>
|
(cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted,
|
||||||
cipher.type === CipherType.SshKey &&
|
|
||||||
!cipher.isDeleted &&
|
|
||||||
cipher.organizationId === null,
|
|
||||||
);
|
);
|
||||||
const keys = sshCiphers.map((cipher) => {
|
const keys = sshCiphers.map((cipher) => {
|
||||||
return {
|
return {
|
||||||
@@ -217,11 +151,88 @@ export class SshAgentService implements OnDestroy {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
await ipc.platform.sshAgent.setKeys(keys);
|
await ipc.platform.sshAgent.setKeys(keys);
|
||||||
}),
|
await ipc.platform.sshAgent.signRequestResponse(requestId, true);
|
||||||
takeUntil(this.destroy$),
|
return;
|
||||||
)
|
}
|
||||||
.subscribe();
|
|
||||||
}
|
if (ciphers === undefined) {
|
||||||
|
ipc.platform.sshAgent
|
||||||
|
.signRequestResponse(requestId, false)
|
||||||
|
.catch((e) => this.logService.error("Failed to respond to SSH request", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = ciphers.find((cipher) => cipher.id == cipherId);
|
||||||
|
|
||||||
|
ipc.platform.focusWindow();
|
||||||
|
const dialogRef = ApproveSshRequestComponent.open(
|
||||||
|
this.dialogService,
|
||||||
|
cipher.name,
|
||||||
|
application,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(dialogRef.closed);
|
||||||
|
return ipc.platform.sshAgent.signRequestResponse(requestId, result);
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.accountService.activeAccount$.pipe(skip(1), takeUntil(this.destroy$)).subscribe({
|
||||||
|
next: (account) => {
|
||||||
|
this.logService.info("Active account changed, clearing SSH keys");
|
||||||
|
ipc.platform.sshAgent
|
||||||
|
.clearKeys()
|
||||||
|
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||||
|
},
|
||||||
|
error: (e: unknown) => {
|
||||||
|
this.logService.error("Error in active account observable", e);
|
||||||
|
ipc.platform.sshAgent
|
||||||
|
.clearKeys()
|
||||||
|
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.logService.info("Active account observable completed, clearing SSH keys");
|
||||||
|
ipc.platform.sshAgent
|
||||||
|
.clearKeys()
|
||||||
|
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
timer(0, this.SSH_REFRESH_INTERVAL),
|
||||||
|
this.desktopSettingsService.sshAgentEnabled$,
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
concatMap(async ([, enabled]) => {
|
||||||
|
if (!enabled) {
|
||||||
|
await ipc.platform.sshAgent.clearKeys();
|
||||||
|
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 &&
|
||||||
|
cipher.organizationId === null,
|
||||||
|
);
|
||||||
|
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() {
|
ngOnDestroy() {
|
||||||
|
|||||||
Reference in New Issue
Block a user