diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0065ca2a3e..e466891ad9 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -668,6 +668,7 @@ dependencies = [ "libsecret", "pin-project", "rand", + "rand_chacha", "retry", "russh-cryptovec", "scopeguard", diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 663bd55e2b..bbbac4a7bb 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -41,6 +41,7 @@ tokio = { version = "1.38.0", features = ["io-util", "sync", "macros", "net"] } tokio-stream = { version = "0.1.15", features = ["net"] } tokio-util = "0.7.11" typenum = "=1.17.0" +rand_chacha = "0.3.1" [target.'cfg(windows)'.dependencies] widestring = "=1.1.0" diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs new file mode 100644 index 0000000000..569d0afd6a --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs @@ -0,0 +1,46 @@ +use rand::SeedableRng; +use rand_chacha::ChaCha8Rng; +use ssh_key::{Algorithm, HashAlg, LineEnding}; + +use super::importer::SshKey; + +pub async fn generate_keypair(key_algorithm: String) -> Result { + // sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom + // if it cannot be securely sourced, this will panic instead of leading to a weak key + let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy(); + + let key = match key_algorithm.as_str() { + "ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519), + "rsa2048" | "rsa3072" | "rsa4096" => { + let bits = match key_algorithm.as_str() { + "rsa2048" => 2048, + "rsa3072" => 3072, + "rsa4096" => 4096, + _ => return Err(anyhow::anyhow!("Unsupported RSA key size")), + }; + let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + + let private_key = ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::from(rsa_keypair), + "".to_string(), + ) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + Ok(private_key) + } + _ => { + return Err(anyhow::anyhow!("Unsupported key algorithm")); + } + } + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + + let private_key_openssh = key + .to_openssh(LineEnding::LF) + .or_else(|e| Err(anyhow::anyhow!(e.to_string())))?; + Ok(SshKey { + private_key: private_key_openssh.to_string(), + public_key: key.public_key().to_string(), + key_algorithm: key_algorithm.to_string(), + key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), + }) +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs new file mode 100644 index 0000000000..48cdecef9c --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs @@ -0,0 +1,76 @@ +use ssh_key::{HashAlg, LineEnding}; + +pub fn import_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(_) => { + 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_algorithm: private_key.algorithm().to_string(), + key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), + }), + }), + Err(_) => Ok(SshKeyImportResult { + status: SshKeyImportStatus::ParsingError, + ssh_key: None, + }), + } +} + +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, +} + +pub struct SshKeyImportResult { + pub status: SshKeyImportStatus, + pub ssh_key: Option, +} + +pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_algorithm: String, + pub key_fingerprint: String, +} 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 84d6c2c859..36e89fc37a 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -10,6 +10,9 @@ use bitwarden_russh::ssh_agent::{self, Key}; #[cfg_attr(target_os = "linux", path = "unix.rs")] mod platform_ssh_agent; +pub mod generator; +pub mod importer; + #[derive(Clone)] pub struct BitwardenDesktopAgent { keystore: ssh_agent::KeyStore, diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index 3ca4430367..41fba3c51c 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -1,10 +1,12 @@ -use std::{collections::HashMap, sync::{Arc, RwLock}}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use bitwarden_russh::ssh_agent; use homedir::my_home; -use tokio_util::sync::CancellationToken; use tokio::{net::UnixListener, sync::Mutex}; - +use tokio_util::sync::CancellationToken; use super::BitwardenDesktopAgent; @@ -36,7 +38,7 @@ impl BitwardenDesktopAgent { .join(".bitwarden-ssh-agent.sock") .to_str() .expect("Path should be valid") - .to_string() + .to_owned() } }; @@ -71,4 +73,4 @@ impl BitwardenDesktopAgent { Ok(agent) } -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs index 024767aa40..d315ad53b8 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs @@ -2,9 +2,12 @@ use async_stream::stream; use futures::stream::{Stream, StreamExt}; pub mod namedpipelistenerstream; -use std::{collections::HashMap, sync::{Arc, RwLock}}; -use tokio_util::sync::CancellationToken; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use tokio::{net::UnixListener, sync::Mutex}; +use tokio_util::sync::CancellationToken; impl BitwardenDesktopAgent { pub async fn start_server( @@ -33,4 +36,4 @@ impl BitwardenDesktopAgent { }); Ok(agent_state) } -} \ No newline at end of file +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 7b43400451..14a3685908 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -165,6 +165,11 @@ pub mod sshagent { use ssh_key::{rand_core::SeedableRng, Algorithm, HashAlg, LineEnding}; use tokio::{self, sync::Mutex}; + #[napi] + pub struct SshAgentState { + state: desktop_core::ssh_agent::BitwardenDesktopAgent, + } + #[napi(object)] pub struct PrivateKey { pub private_key: String, @@ -180,9 +185,15 @@ pub mod sshagent { pub key_fingerprint: String, } - #[napi] - pub struct SshAgentState { - state: desktop_core::ssh_agent::BitwardenDesktopAgent, + 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_algorithm: key.key_algorithm, + key_fingerprint: key.key_fingerprint, + } + } } #[napi] @@ -197,12 +208,40 @@ pub mod sshagent { ParsingError, } + 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 + } + } + } + } + #[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, @@ -276,110 +315,17 @@ pub mod sshagent { #[napi] pub fn import_key(encoded_key: String, password: String) -> napi::Result { - let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key); - let private_key = match private_key { - Ok(k) => k, - Err(_) => { - 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 - }; - let public_key = private_key.public_key(); - let public_key_base64 = public_key.to_string(); - let private_key_openssh = private_key - .to_openssh(LineEnding::LF) - .or_else(|e| Err(napi::Error::from_reason(e.to_string())))?; - let private_key_openssh_string = private_key_openssh.to_string(); - let fingerprint = private_key.fingerprint(HashAlg::Sha256); - let fingerprint_string = fingerprint.to_string(); - Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key_openssh_string, - public_key: public_key_base64, - key_algorithm: private_key.algorithm().to_string(), - key_fingerprint: fingerprint_string, - }), - }) + 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 async fn generate_keypair(key_algorithm: String) -> napi::Result { - // sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom - // if it cannot be securely sourced, this will panic instead of leading to a weak key - let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy(); - - let key = match key_algorithm.as_str() { - "ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519) - .or_else(|e| Err(napi::Error::from_reason(e.to_string()))), - "rsa2048" | "rsa3072" | "rsa4096" => { - let bits = match key_algorithm.as_str() { - "rsa2048" => 2048, - "rsa3072" => 3072, - "rsa4096" => 4096, - _ => Err(napi::Error::from_reason( - "Unsupported RSA key size".to_string(), - ))?, - }; - let rsa_keypair: Result = - ssh_key::private::RsaKeypair::random(&mut rng, bits) - .or_else(|e| Err(napi::Error::from_reason(e.to_string()))?); - let rsa_keypair = rsa_keypair?; - let private_key = ssh_key::PrivateKey::new( - ssh_key::private::KeypairData::from(rsa_keypair), - "".to_string(), - ) - .or_else(|e| Err(napi::Error::from_reason(e.to_string()))); - private_key - } - _ => { - return Err(napi::Error::from_reason( - "Unsupported key algorithm".to_string(), - )) - } - }; - - match key { - Ok(key) => { - let public_key = key.public_key(); - let public_key_base64 = public_key.to_string(); - let private_key_openssh = key - .to_openssh(LineEnding::LF) - .or_else(|e| Err(napi::Error::from_reason(e.to_string())))?; - let private_key_openssh_string = private_key_openssh.to_string(); - let fingerprint = key.fingerprint(HashAlg::Sha256); - let fingerprint_string = fingerprint.to_string(); - Ok(SshKey { - private_key: private_key_openssh_string, - public_key: public_key_base64, - key_algorithm: key_algorithm.to_string(), - key_fingerprint: fingerprint_string, - }) - } - Err(e) => Err(napi::Error::from_reason(e.to_string())), - } + desktop_core::ssh_agent::generator::generate_keypair(key_algorithm) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|k| k.into()) } }