From f79141c4213a4a205399e7ada1e1cbd5ddf45f36 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 27 Nov 2024 08:29:36 -0800 Subject: [PATCH] [PM-14990] Add password prompt for ssh key import (#12105) * Add password prompt for ssh key import * Remove empty line * Convert to switch statement --- apps/desktop/src/locales/en/messages.json | 19 ++++- .../src/vault/app/vault/add-edit.component.ts | 80 ++++++++++++------- libs/importer/src/components/dialog/index.ts | 1 + .../sshkey-password-prompt.component.html | 31 +++++++ .../sshkey-password-prompt.component.ts | 46 +++++++++++ 5 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 libs/importer/src/components/dialog/sshkey-password-prompt.component.html create mode 100644 libs/importer/src/components/dialog/sshkey-password-prompt.component.ts diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 837535ddb0..e9bebb8bfc 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -207,6 +207,21 @@ "sshKeyGenerated": { "message": "A new SSH key was generated" }, + "sshKeyWrongPassword": { + "message": "The password you entered is incorrect." + }, + "importSshKey": { + "message": "Import" + }, + "confirmSshKeyPassword": { + "message": "Confirm password" + }, + "enterSshKeyPasswordDesc": { + "message": "Enter the password for the SSH key." + }, + "enterSshKeyPassword": { + "message": "Enter password" + }, "sshAgentUnlockRequired": { "message": "Please unlock your vault to approve the SSH key request." }, @@ -1752,10 +1767,10 @@ "deleteAccountWarning": { "message": "Deleting your account is permanent. It cannot be undone." }, - "cannotDeleteAccount":{ + "cannotDeleteAccount": { "message": "Cannot delete account" }, - "cannotDeleteAccountDesc":{ + "cannotDeleteAccountDesc": { "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." }, "accountDeleted": { diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index 015a7c6b21..6a3ad8d62e 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -2,6 +2,7 @@ import { DatePipe } from "@angular/common"; import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { NgForm } from "@angular/forms"; import { sshagent as sshAgent } from "desktop_native/napi"; +import { lastValueFrom } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; @@ -22,6 +23,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui"; import { PasswordRepromptService } from "@bitwarden/vault"; const BroadcasterSubscriptionId = "AddEditComponent"; @@ -170,42 +172,64 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On } } - async importSshKeyFromClipboard() { + async importSshKeyFromClipboard(password: string = "") { const key = await this.platformUtilsService.readFromClipboard(); - const parsedKey = await ipc.platform.sshAgent.importKey(key, ""); - if (parsedKey == null || parsedKey.status === sshAgent.SshKeyImportStatus.ParsingError) { + const parsedKey = await ipc.platform.sshAgent.importKey(key, password); + if (parsedKey == null) { this.toastService.showToast({ variant: "error", title: "", message: this.i18nService.t("invalidSshKey"), }); return; - } else if (parsedKey.status === sshAgent.SshKeyImportStatus.UnsupportedKeyType) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("sshKeyTypeUnsupported"), - }); - } else if ( - parsedKey.status === sshAgent.SshKeyImportStatus.PasswordRequired || - parsedKey.status === sshAgent.SshKeyImportStatus.WrongPassword - ) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("sshKeyPasswordUnsupported"), - }); - return; - } else { - this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey; - this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey; - this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint; - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("sshKeyPasted"), - }); } + + switch (parsedKey.status) { + case sshAgent.SshKeyImportStatus.ParsingError: + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("invalidSshKey"), + }); + return; + case sshAgent.SshKeyImportStatus.UnsupportedKeyType: + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("sshKeyTypeUnsupported"), + }); + return; + case sshAgent.SshKeyImportStatus.PasswordRequired: + case sshAgent.SshKeyImportStatus.WrongPassword: + if (password !== "") { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("sshKeyWrongPassword"), + }); + } else { + password = await this.getSshKeyPassword(); + await this.importSshKeyFromClipboard(password); + } + return; + default: + this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey; + this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey; + this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyPasted"), + }); + } + } + + async getSshKeyPassword(): Promise { + const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { + ariaModal: true, + }); + + return await lastValueFrom(dialog.closed); } async typeChange() { diff --git a/libs/importer/src/components/dialog/index.ts b/libs/importer/src/components/dialog/index.ts index 641cd6600a..a115426eea 100644 --- a/libs/importer/src/components/dialog/index.ts +++ b/libs/importer/src/components/dialog/index.ts @@ -1,3 +1,4 @@ export * from "./import-error-dialog.component"; export * from "./import-success-dialog.component"; export * from "./file-password-prompt.component"; +export * from "./sshkey-password-prompt.component"; diff --git a/libs/importer/src/components/dialog/sshkey-password-prompt.component.html b/libs/importer/src/components/dialog/sshkey-password-prompt.component.html new file mode 100644 index 0000000000..a42615b1cd --- /dev/null +++ b/libs/importer/src/components/dialog/sshkey-password-prompt.component.html @@ -0,0 +1,31 @@ +
+ + + {{ "enterSshKeyPassword" | i18n }} + + +
+ {{ "enterSshKeyPasswordDesc" | i18n }} + + {{ "confirmSshKeyPassword" | i18n }} + + + +
+ + + + + +
+
diff --git a/libs/importer/src/components/dialog/sshkey-password-prompt.component.ts b/libs/importer/src/components/dialog/sshkey-password-prompt.component.ts new file mode 100644 index 0000000000..527dfec6e8 --- /dev/null +++ b/libs/importer/src/components/dialog/sshkey-password-prompt.component.ts @@ -0,0 +1,46 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + FormFieldModule, + IconButtonModule, +} from "@bitwarden/components"; + +@Component({ + templateUrl: "sshkey-password-prompt.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + DialogModule, + FormFieldModule, + AsyncActionsModule, + ButtonModule, + IconButtonModule, + ReactiveFormsModule, + ], +}) +export class SshKeyPasswordPromptComponent { + protected formGroup = this.formBuilder.group({ + sshKeyPassword: ["", Validators.required], + }); + + constructor( + public dialogRef: DialogRef, + protected formBuilder: FormBuilder, + ) {} + + submit = () => { + this.formGroup.markAsTouched(); + if (!this.formGroup.valid) { + return; + } + this.dialogRef.close(this.formGroup.value.sshKeyPassword); + }; +}