diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 51fb3a0a770..978cdae96b1 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -176,7 +176,7 @@ "copyNotes": { "message": "Copy notes" }, - "fill":{ + "fill": { "message": "Fill", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, @@ -4903,5 +4903,32 @@ }, "extraWide": { "message": "Extra wide" + }, + "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" + }, + "invalidSshKey": { + "message": "The SSH key is invalid" + }, + "sshKeyTypeUnsupported": { + "message": "The SSH key type is not supported" + }, + "importSshKeyFromClipboard": { + "message": "Import key from clipboard" + }, + "sshKeyPasted": { + "message": "SSH key imported successfully" } } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs deleted file mode 100644 index 52464487ec5..00000000000 --- a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs +++ /dev/null @@ -1,402 +0,0 @@ -use ed25519; -use pkcs8::{ - der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument, -}; -use ssh_key::{ - private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair}, - HashAlg, LineEnding, -}; - -const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----"; -const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----"; -const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; -const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----"; - -pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier = - ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); - -#[derive(Debug)] -enum KeyType { - Ed25519, - Rsa, - Unknown, -} - -pub fn import_key( - encoded_key: String, - password: String, -) -> Result { - match encoded_key.lines().next() { - Some(PKCS1_HEADER) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::UnsupportedKeyType, - ssh_key: None, - }), - Some(PKCS8_UNENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, None) { - Ok(result) => Ok(result), - Err(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - }, - Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) { - Ok(result) => Ok(result), - Err(err) => match err { - SshKeyImportError::PasswordRequired => Ok(SshKeyImportResult { - status: SshKeyImportStatus::PasswordRequired, - ssh_key: None, - }), - SshKeyImportError::WrongPassword => Ok(SshKeyImportResult { - status: SshKeyImportStatus::WrongPassword, - ssh_key: None, - }), - SshKeyImportError::ParsingError => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - }, - }, - Some(OPENSSH_HEADER) => import_openssh_key(encoded_key, password), - Some(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - None => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - } -} - -fn import_pkcs8_key( - encoded_key: String, - password: Option, -) -> Result { - let der = match SecretDocument::from_pem(&encoded_key) { - Ok((_, doc)) => doc, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }); - } - }; - - let decrypted_der = match password.clone() { - Some(password) => { - let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes()) - { - Ok(info) => info, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }); - } - }; - match encrypted_private_key_info.decrypt(password.as_bytes()) { - Ok(der) => der, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::WrongPassword, - ssh_key: None, - }); - } - } - } - None => der, - }; - - let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes()) - .map_err(|_| SshKeyImportError::ParsingError)? - .algorithm - .oid - { - ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519, - RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa, - _ => KeyType::Unknown, - }; - - match key_type { - KeyType::Ed25519 => { - let pk: ed25519::KeypairBytes = match password { - Some(password) => { - pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) - .map_err(|err| match err { - ed25519::pkcs8::Error::EncryptedPrivateKey(_) => { - SshKeyImportError::WrongPassword - } - _ => SshKeyImportError::ParsingError, - })? - } - None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) - .map_err(|_| SshKeyImportError::ParsingError)?, - }; - let pk: Ed25519Keypair = - Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key)); - let private_key = ssh_key::private::PrivateKey::from(pk); - Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(), - public_key: private_key.public_key().to_string(), - key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), - }), - }) - } - KeyType::Rsa => { - let pk: rsa::RsaPrivateKey = match password { - Some(password) => { - pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) - .map_err(|err| match err { - pkcs8::Error::EncryptedPrivateKey(_) => { - SshKeyImportError::WrongPassword - } - _ => SshKeyImportError::ParsingError, - })? - } - None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) - .map_err(|_| SshKeyImportError::ParsingError)?, - }; - let rsa_keypair: Result = RsaKeypair::try_from(pk); - match rsa_keypair { - Ok(rsa_keypair) => { - let private_key = ssh_key::private::PrivateKey::from(rsa_keypair); - Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key - .to_openssh(LineEnding::LF) - .unwrap() - .to_string(), - public_key: private_key.public_key().to_string(), - key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), - }), - }) - } - Err(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - } - } - _ => Ok(SshKeyImportResult { - status: SshKeyImportStatus::UnsupportedKeyType, - ssh_key: None, - }), - } -} - -fn import_openssh_key( - encoded_key: String, - password: String, -) -> Result { - let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key); - let private_key = match private_key { - Ok(k) => k, - Err(err) => { - match err { - ssh_key::Error::AlgorithmUnknown - | ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::UnsupportedKeyType, - ssh_key: None, - }); - } - _ => {} - } - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }); - } - }; - - if private_key.is_encrypted() && password.is_empty() { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::PasswordRequired, - ssh_key: None, - }); - } - let private_key = if private_key.is_encrypted() { - match private_key.decrypt(password.as_bytes()) { - Ok(k) => k, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::WrongPassword, - ssh_key: None, - }); - } - } - } else { - private_key - }; - - match private_key.to_openssh(LineEnding::LF) { - Ok(private_key_openssh) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key_openssh.to_string(), - public_key: private_key.public_key().to_string(), - key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), - }), - }), - Err(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - } -} - -#[derive(PartialEq, Debug)] -pub enum SshKeyImportStatus { - /// ssh key was parsed correctly and will be returned in the result - Success, - /// ssh key was parsed correctly but is encrypted and requires a password - PasswordRequired, - /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect - WrongPassword, - /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key - ParsingError, - /// ssh key type is not supported - UnsupportedKeyType, -} - -pub enum SshKeyImportError { - ParsingError, - PasswordRequired, - WrongPassword, -} - -pub struct SshKeyImportResult { - pub status: SshKeyImportStatus, - pub ssh_key: Option, -} - -pub struct SshKey { - pub private_key: String, - pub public_key: String, - pub key_fingerprint: String, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn import_key_ed25519_openssh_unencrypted() { - let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted"); - let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_ed25519_openssh_encrypted() { - let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); - let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim(); - let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_openssh_unencrypted() { - let private_key = include_str!("./test_keys/rsa_openssh_unencrypted"); - let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_openssh_encrypted() { - let private_key = include_str!("./test_keys/rsa_openssh_encrypted"); - let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim(); - let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_ed25519_pkcs8_unencrypted() { - let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted"); - let public_key = - include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", ""); - let public_key = public_key.trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_pkcs8_unencrypted() { - let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted"); - // for whatever reason pkcs8 + rsa does not include the comment in the public key - let public_key = - include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", ""); - let public_key = public_key.trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_pkcs8_encrypted() { - let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted"); - let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", ""); - let public_key = public_key.trim(); - let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_ed25519_openssh_encrypted_wrong_password() { - let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); - let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::WrongPassword); - } - - #[test] - fn import_non_key_error() { - let result = import_key("not a key".to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::ParsingError); - } - - #[test] - fn import_ecdsa_error() { - let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType); - } - - // Putty-exported keys should be supported, but are not due to a parser incompatibility. - // Should this test start failing, please change it to expect a correct key, and - // make sure the documentation support for putty-exported keys this is updated. - // https://bitwarden.atlassian.net/browse/PM-14989 - #[test] - fn import_key_ed25519_putty() { - let private_key = include_str!("./test_keys/ed25519_putty_openssh_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::ParsingError); - } - - // Putty-exported keys should be supported, but are not due to a parser incompatibility. - // Should this test start failing, please change it to expect a correct key, and - // make sure the documentation support for putty-exported keys this is updated. - // https://bitwarden.atlassian.net/browse/PM-14989 - #[test] - fn import_key_rsa_openssh_putty() { - let private_key = include_str!("./test_keys/rsa_putty_openssh_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::ParsingError); - } - - #[test] - fn import_key_rsa_pkcs8_putty() { - let private_key = include_str!("./test_keys/rsa_putty_pkcs1_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType); - } -} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 4e304ccea78..7e572fe6e14 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -16,7 +16,6 @@ mod platform_ssh_agent; #[cfg(any(target_os = "linux", target_os = "macos"))] mod peercred_unix_listener_stream; -pub mod importer; pub mod peerinfo; #[derive(Clone)] pub struct BitwardenDesktopAgent { diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 997e951c89e..05fd9a961f0 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -51,28 +51,11 @@ export declare namespace sshagent { publicKey: string keyFingerprint: string } - export const enum SshKeyImportStatus { - /** ssh key was parsed correctly and will be returned in the result */ - Success = 0, - /** ssh key was parsed correctly but is encrypted and requires a password */ - PasswordRequired = 1, - /** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */ - WrongPassword = 2, - /** ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key */ - ParsingError = 3, - /** ssh key type is not supported (e.g. ecdsa) */ - UnsupportedKeyType = 4 - } - export interface SshKeyImportResult { - status: SshKeyImportStatus - sshKey?: SshKey - } export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise export function stop(agentState: SshAgentState): void export function isRunning(agentState: SshAgentState): boolean export function setKeys(agentState: SshAgentState, newKeys: Array): void export function lock(agentState: SshAgentState): void - export function importKey(encodedKey: string, password: string): SshKeyImportResult export function clearKeys(agentState: SshAgentState): void export class SshAgentState { } } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 35566e16813..36f54e55be3 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -182,67 +182,6 @@ pub mod sshagent { pub key_fingerprint: String, } - impl From for SshKey { - fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self { - SshKey { - private_key: key.private_key, - public_key: key.public_key, - key_fingerprint: key.key_fingerprint, - } - } - } - - #[napi] - pub enum SshKeyImportStatus { - /// ssh key was parsed correctly and will be returned in the result - Success, - /// ssh key was parsed correctly but is encrypted and requires a password - PasswordRequired, - /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect - WrongPassword, - /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key - ParsingError, - /// ssh key type is not supported (e.g. ecdsa) - UnsupportedKeyType, - } - - impl From for SshKeyImportStatus { - fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self { - match status { - desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => { - SshKeyImportStatus::Success - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => { - SshKeyImportStatus::PasswordRequired - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => { - SshKeyImportStatus::WrongPassword - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => { - SshKeyImportStatus::ParsingError - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => { - SshKeyImportStatus::UnsupportedKeyType - } - } - } - } - - #[napi(object)] - pub struct SshKeyImportResult { - pub status: SshKeyImportStatus, - pub ssh_key: Option, - } - - impl From for SshKeyImportResult { - fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self { - SshKeyImportResult { - status: result.status.into(), - ssh_key: result.ssh_key.map(|k| k.into()), - } - } - } - #[napi] pub async fn serve( callback: ThreadsafeFunction<(Option, bool, String), CalleeHandled>, @@ -348,13 +287,6 @@ pub mod sshagent { .map_err(|e| napi::Error::from_reason(e.to_string())) } - #[napi] - pub fn import_key(encoded_key: String, password: String) -> napi::Result { - let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - Ok(result.into()) - } - #[napi] pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { let bitwarden_agent_state = &mut agent_state.state; diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index bca12f16a7d..c965e5b153e 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3421,9 +3421,6 @@ "unknownApplication": { "message": "An application" }, - "sshKeyPasswordUnsupported": { - "message": "Importing password protected SSH keys is not yet supported" - }, "invalidSshKey": { "message": "The SSH key is invalid" }, diff --git a/apps/desktop/src/platform/main/main-ssh-agent.service.ts b/apps/desktop/src/platform/main/main-ssh-agent.service.ts index cc4565f27f4..b4c4e0c20ff 100644 --- a/apps/desktop/src/platform/main/main-ssh-agent.service.ts +++ b/apps/desktop/src/platform/main/main-ssh-agent.service.ts @@ -25,16 +25,6 @@ export class MainSshAgentService { private logService: LogService, private messagingService: MessagingService, ) { - ipcMain.handle( - "sshagent.importkey", - async ( - event: any, - { privateKey, password }: { privateKey: string; password?: string }, - ): Promise => { - return sshagent.importKey(privateKey, password); - }, - ); - ipcMain.handle("sshagent.init", async (event: any, message: any) => { this.init(); }); diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index a37274677ff..50af4b53b14 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -1,4 +1,3 @@ -import { sshagent as ssh } from "desktop_native/napi"; import { ipcRenderer } from "electron"; import { DeviceType } from "@bitwarden/common/enums"; @@ -64,13 +63,6 @@ const sshAgent = { clearKeys: async () => { return await ipcRenderer.invoke("sshagent.clearkeys"); }, - importKey: async (key: string, password: string): Promise => { - const res = await ipcRenderer.invoke("sshagent.importkey", { - privateKey: key, - password: password, - }); - return res; - }, isLoaded(): Promise { return ipcRenderer.invoke("sshagent.isloaded"); }, 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 02fa8076086..e2e95a7d130 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -3,8 +3,6 @@ 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"; @@ -24,7 +22,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; 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"; @@ -148,69 +145,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On ); } - async importSshKeyFromClipboard(password: string = "") { - const key = await this.platformUtilsService.readFromClipboard(); - const parsedKey = await ipc.platform.sshAgent.importKey(key, password); - if (parsedKey == null) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("invalidSshKey"), - }); - return; - } - - 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(); - if (password === "") { - return; - } - 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); - } - truncateString(value: string, length: number) { return value.length > length ? value.substring(0, length) + "..." : value; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2779c0470e7..941111f5d08 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -35,7 +35,7 @@ "restoreMembers": { "message": "Restore members" }, - "cannotRestoreAccessError":{ + "cannotRestoreAccessError": { "message": "Cannot restore organization access" }, "allApplicationsWithCount": { @@ -1286,8 +1286,8 @@ "yourAccountIsLocked": { "message": "Your account is locked" }, - "uuid":{ - "message" : "UUID" + "uuid": { + "message": "UUID" }, "unlock": { "message": "Unlock" @@ -5743,10 +5743,10 @@ "bulkFilteredMessage": { "message": "Excluded, not applicable for this action" }, - "nonCompliantMembersTitle":{ + "nonCompliantMembersTitle": { "message": "Non-compliant members" }, - "nonCompliantMembersError":{ + "nonCompliantMembersError": { "message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements" }, "fingerprint": { @@ -9163,7 +9163,7 @@ "message": "for Bitwarden using the implementation guide for your Identity Provider.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, - "userProvisioning":{ + "userProvisioning": { "message": "User provisioning" }, "scimIntegration": { @@ -9177,26 +9177,25 @@ "message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, - "bwdc":{ + "bwdc": { "message": "Bitwarden Directory Connector" }, "bwdcDesc": { "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, - "eventManagement":{ + "eventManagement": { "message": "Event management" }, - "eventManagementDesc":{ + "eventManagementDesc": { "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." }, - "deviceManagement":{ + "deviceManagement": { "message": "Device management" }, - "deviceManagementDesc":{ + "deviceManagementDesc": { "message": "Configure device management for Bitwarden using the implementation guide for your platform." - }, - "integrationCardTooltip":{ + "integrationCardTooltip": { "message": "Launch $INTEGRATION$ implementation guide.", "placeholders": { "integration": { @@ -9205,7 +9204,7 @@ } } }, - "smIntegrationTooltip":{ + "smIntegrationTooltip": { "message": "Set up $INTEGRATION$.", "placeholders": { "integration": { @@ -9214,7 +9213,7 @@ } } }, - "smSdkTooltip":{ + "smSdkTooltip": { "message": "View $SDK$ repository", "placeholders": { "sdk": { @@ -9223,7 +9222,7 @@ } } }, - "integrationCardAriaLabel":{ + "integrationCardAriaLabel": { "message": "open $INTEGRATION$ implementation guide in a new tab.", "placeholders": { "integration": { @@ -9232,7 +9231,7 @@ } } }, - "smSdkAriaLabel":{ + "smSdkAriaLabel": { "message": "view $SDK$ repository in a new tab.", "placeholders": { "sdk": { @@ -9241,7 +9240,7 @@ } } }, - "smIntegrationCardAriaLabel":{ + "smIntegrationCardAriaLabel": { "message": "set up $INTEGRATION$ implementation guide in a new tab.", "placeholders": { "integration": { @@ -9645,7 +9644,7 @@ "message": "Config" }, "learnMoreAboutEmergencyAccess": { - "message":"Learn more about emergency access" + "message": "Learn more about emergency access" }, "learnMoreAboutMatchDetection": { "message": "Learn more about match detection" @@ -9936,7 +9935,7 @@ "selfHostingTitleProper": { "message": "Self-Hosting" }, - "claim-domain-single-org-warning" : { + "claim-domain-single-org-warning": { "message": "Claiming a domain will turn on the single organization policy." }, "single-org-revoked-user-warning": { @@ -10168,6 +10167,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "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" + }, + "invalidSshKey": { + "message": "The SSH key is invalid" + }, + "sshKeyTypeUnsupported": { + "message": "The SSH key type is not supported" + }, + "importSshKeyFromClipboard": { + "message": "Import key from clipboard" + }, + "sshKeyPasted": { + "message": "SSH key imported successfully" + }, "resellerRenewalWarningMsg": { "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index bf2e68b71cd..abd3d23979f 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -2,7 +2,15 @@ // @ts-strict-ignore import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { concatMap, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; +import { + concatMap, + firstValueFrom, + lastValueFrom, + map, + Observable, + Subject, + takeUntil, +} from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -42,7 +50,13 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note. import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { generate_ssh_key } from "@bitwarden/sdk-internal"; +import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui"; +import { + SshKey, + SshKeyImportError, + import_ssh_key, + generate_ssh_key, +} from "@bitwarden/sdk-internal"; import { PasswordRepromptService } from "@bitwarden/vault"; @Directive() @@ -135,7 +149,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected configService: ConfigService, protected cipherAuthorizationService: CipherAuthorizationService, protected toastService: ToastService, - private sdkService: SdkService, + protected sdkService: SdkService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, @@ -802,6 +816,71 @@ export class AddEditComponent implements OnInit, OnDestroy { return true; } + private async importUsingSdk(key: string, password: string): Promise { + await firstValueFrom(this.sdkService.client$); + return import_ssh_key(key, password); + } + + async importSshKeyFromClipboard(password: string = "") { + const ATTEMPTS = 5; + const key = await this.platformUtilsService.readFromClipboard(); + + let parsedKey: SshKey = null; + for (let attempt = 0; attempt <= ATTEMPTS; attempt++) { + try { + parsedKey = await this.importUsingSdk(key, password); + } catch (e) { + const error = e as SshKeyImportError; + if ( + error.variant === "WrongPassword" && + ((password === "" && attempt === 0) || (password !== "" && attempt < ATTEMPTS)) + ) { + password = await this.getSshKeyPassword(); + } else { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t(this.sshImportErrorVariantToI18nKey(error.variant)), + }); + return; + } + continue; + } + break; + } + + this.cipher.sshKey.privateKey = parsedKey.private_key; + this.cipher.sshKey.publicKey = parsedKey.public_key; + this.cipher.sshKey.keyFingerprint = parsedKey.key_fingerprint; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyPasted"), + }); + } + + private sshImportErrorVariantToI18nKey(variant: string): string { + switch (variant) { + case "ParsingError": + return "invalidSshKey"; + case "UnsupportedKeyType": + return "sshKeyTypeUnsupported"; + case "PasswordRequired": + case "WrongPassword": + return "sshKeyWrongPassword"; + default: + return "errorOccurred"; + } + } + + async getSshKeyPassword(): Promise { + const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { + ariaModal: true, + }); + + return await lastValueFrom(dialog.closed); + } + private async generateSshKey(showNotification: boolean = true) { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); diff --git a/libs/common/src/models/export/ssh-key.export.ts b/libs/common/src/models/export/ssh-key.export.ts index a99ebac34b3..72b1c306c20 100644 --- a/libs/common/src/models/export/ssh-key.export.ts +++ b/libs/common/src/models/export/ssh-key.export.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { import_ssh_key } from "@bitwarden/sdk-internal"; import { EncString } from "../../platform/models/domain/enc-string"; import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key"; @@ -24,9 +25,10 @@ export class SshKeyExport { } static toDomain(req: SshKeyExport, domain = new SshKeyDomain()) { - domain.privateKey = req.privateKey != null ? new EncString(req.privateKey) : null; - domain.publicKey = req.publicKey != null ? new EncString(req.publicKey) : null; - domain.keyFingerprint = req.keyFingerprint != null ? new EncString(req.keyFingerprint) : null; + const parsedKey = import_ssh_key(req.privateKey); + domain.privateKey = new EncString(parsedKey.private_key); + domain.publicKey = new EncString(parsedKey.public_key); + domain.keyFingerprint = new EncString(parsedKey.key_fingerprint); return domain; } diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html index 51b07a1cbf3..b3abfc0466b 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html @@ -15,6 +15,13 @@

data-testid="toggle-privateKey-visibility" bitPasswordInputToggle > + diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index 134897c9356..81ccaab378b 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -4,23 +4,33 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CardComponent, + DialogService, FormFieldModule, IconButtonModule, SectionComponent, SectionHeaderComponent, SelectModule, + ToastService, TypographyModule, } from "@bitwarden/components"; -import { generate_ssh_key } from "@bitwarden/sdk-internal"; +import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui"; +import { + SshKey, + SshKeyImportError, + import_ssh_key, + generate_ssh_key, +} from "@bitwarden/sdk-internal"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -64,6 +74,10 @@ export class SshKeySectionComponent implements OnInit { private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, private i18nService: I18nService, + private toastService: ToastService, + private platformUtilsService: PlatformUtilsService, + private logService: LogService, + private dialogService: DialogService, private sdkService: SdkService, ) { this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm); @@ -100,6 +114,73 @@ export class SshKeySectionComponent implements OnInit { }); } + private async importUsingSdk(key: string, password: string): Promise { + await firstValueFrom(this.sdkService.client$); + return import_ssh_key(key, password); + } + + async importSshKeyFromClipboard(password: string = "") { + const ATTEMPTS = 5; + const key = await this.platformUtilsService.readFromClipboard(); + + let parsedKey: SshKey = null; + for (let attempt = 0; attempt <= ATTEMPTS; attempt++) { + try { + parsedKey = await this.importUsingSdk(key, password); + } catch (e) { + const error = e as SshKeyImportError; + if ( + error.variant === "WrongPassword" && + ((password === "" && attempt === 0) || (password !== "" && attempt < ATTEMPTS)) + ) { + password = await this.getSshKeyPassword(); + } else { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t(this.sshImportErrorVariantToI18nKey(error.variant)), + }); + return; + } + continue; + } + break; + } + + this.sshKeyForm.setValue({ + privateKey: parsedKey.private_key, + publicKey: parsedKey.public_key, + keyFingerprint: parsedKey.key_fingerprint, + }); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyPasted"), + }); + } + + private sshImportErrorVariantToI18nKey(variant: string): string { + switch (variant) { + case "ParsingError": + return "invalidSshKey"; + case "UnsupportedKeyType": + return "sshKeyTypeUnsupported"; + case "PasswordRequired": + case "WrongPassword": + return "sshKeyWrongPassword"; + default: + return "errorOccurred"; + } + } + + async getSshKeyPassword(): Promise { + const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { + ariaModal: true, + }); + + return await lastValueFrom(dialog.closed); + } private async generateSshKey() { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index 8318212e81d..e9204a025f1 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -8,6 +8,7 @@ "@bitwarden/auth/common": ["../auth/src/common"], "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], + "@bitwarden/importer/ui": ["../importer/src/components"], "@bitwarden/generator-components": ["../tools/generator/components/src"], "@bitwarden/generator-core": ["../tools/generator/core/src"], "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], diff --git a/package-lock.json b/package-lock.json index 3d6ff011724..126b929d744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.38", + "@bitwarden/sdk-internal": "0.2.0-main.47", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -4478,9 +4478,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.38", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.38.tgz", - "integrity": "sha512-bkN+BZC0YA4k0To8QiT33UTZX8peKDXud8Gzq3UHNPlU/vMSkP3Wn8q0GezzmYN3UNNIWXfreNCS0mJ+S51j/Q==", + "version": "0.2.0-main.47", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.47.tgz", + "integrity": "sha512-alISSoMpAEJD/4+vwjv8kwK4/skd+yqA8pXjZJqMlkVSnp1rtT0ZN+6TC91WmHsJTjjjDwt7DrT8YOkCuwmdnQ==", "license": "GPL-3.0" }, "node_modules/@bitwarden/vault": { diff --git a/package.json b/package.json index e6c476d2dfd..911a4f0c7a5 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.38", + "@bitwarden/sdk-internal": "0.2.0-main.47", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2",