diff --git a/Cargo.lock b/Cargo.lock index 9ac7b86a4..a81c75402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -974,7 +974,7 @@ dependencies = [ "hashbrown 0.14.0", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.8", ] [[package]] @@ -1805,6 +1805,7 @@ dependencies = [ "similar", "slug", "ssh-key", + "ssh2", "tauri", "tauri-build", "tauri-plugin-context-menu", @@ -1911,7 +1912,7 @@ dependencies = [ "futures-timer", "no-std-compat", "nonzero_ext", - "parking_lot", + "parking_lot 0.12.1", "quanta", "rand 0.8.5", "smallvec", @@ -2975,7 +2976,7 @@ dependencies = [ "file-id", "log", "notify", - "parking_lot", + "parking_lot 0.12.1", "walkdir", ] @@ -3293,6 +3294,17 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -3300,7 +3312,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -3749,7 +3775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot", + "parking_lot 0.12.1", "scheduled-thread-pool", ] @@ -4214,7 +4240,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -4767,6 +4793,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ssh2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libssh2-sys", + "parking_lot 0.11.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4796,7 +4834,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -4937,7 +4975,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "png", "raw-window-handle", "scopeguard", @@ -5335,7 +5373,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", diff --git a/packages/tauri/Cargo.toml b/packages/tauri/Cargo.toml index 229807c73..eaf919716 100644 --- a/packages/tauri/Cargo.toml +++ b/packages/tauri/Cargo.toml @@ -58,6 +58,7 @@ sha2 = "0.10.8" similar = { version = "2.2.1", features = ["unicode"] } slug = "0.1.5" ssh-key = { version = "0.6.3", features = [ "alloc", "ed25519" ] } +ssh2 = { version = "0.9.4", features = ["vendored-openssl"] } tauri = { version = "1.5.2", features = ["dialog-open", "fs-read-file", "path-all", "process-relaunch", "protocol-asset", "shell-open", "system-tray", "window-maximize", "window-start-dragging", "window-unmaximize"] } tauri-plugin-context-menu = { git = "https://github.com/gitbutlerapp/tauri-plugin-context-menu", branch = "main" } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } diff --git a/packages/tauri/src/git/url.rs b/packages/tauri/src/git/url.rs index fbcaeb8f4..e11b7f81a 100644 --- a/packages/tauri/src/git/url.rs +++ b/packages/tauri/src/git/url.rs @@ -18,7 +18,7 @@ pub struct Url { /// The password associated with a user. password: Option, /// The host to which to connect. Localhost is implied if `None`. - host: Option, + pub host: Option, /// When serializing, use the alternative forms as it was parsed as such. serialize_alternative_form: bool, /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used. diff --git a/packages/tauri/src/lib.rs b/packages/tauri/src/lib.rs index bfe7701e9..265c914a0 100644 --- a/packages/tauri/src/lib.rs +++ b/packages/tauri/src/lib.rs @@ -22,6 +22,7 @@ pub mod projects; pub mod reader; pub mod sentry; pub mod sessions; +pub mod ssh; pub mod storage; pub mod users; pub mod virtual_branches; diff --git a/packages/tauri/src/project_repository/repository.rs b/packages/tauri/src/project_repository/repository.rs index 182261783..ba7dda4c1 100644 --- a/packages/tauri/src/project_repository/repository.rs +++ b/packages/tauri/src/project_repository/repository.rs @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use crate::{ git::{self, credentials::HelpError, Url}, - keys, projects, reader, users, + keys, projects, reader, ssh, users, virtual_branches::Branch, }; @@ -352,6 +352,9 @@ impl Repository { let auth_flows = credentials.help(self, branch.remote())?; for (mut remote, callbacks) in auth_flows { + if let Some(url) = remote.url().context("failed to get remote url")? { + ssh::check_known_host(&url).context("failed to check known host")?; + } for callback in callbacks { match remote.push( &[refspec.as_str()], @@ -391,6 +394,9 @@ impl Repository { let refspec = &format!("+refs/heads/*:refs/remotes/{}/*", remote_name); let auth_flows = credentials.help(self, remote_name)?; for (mut remote, callbacks) in auth_flows { + if let Some(url) = remote.url().context("failed to get remote url")? { + ssh::check_known_host(&url).context("failed to check known host")?; + } for callback in callbacks { let mut fetch_opts = git2::FetchOptions::new(); fetch_opts.remote_callbacks(callback.into()); diff --git a/packages/tauri/src/ssh.rs b/packages/tauri/src/ssh.rs new file mode 100644 index 000000000..36471edfa --- /dev/null +++ b/packages/tauri/src/ssh.rs @@ -0,0 +1,63 @@ +use std::{env, fs, path::Path}; + +use ssh2::{self, CheckResult, KnownHostFileKind}; + +use crate::git; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Ssh(ssh2::Error), + #[error(transparent)] + Io(std::io::Error), + #[error("mismatched host key")] + MismatchedHostKey, + #[error("failed to check the known hosts")] + Failure, +} + +pub fn check_known_host(remote_url: &git::Url) -> Result<(), Error> { + let host = if let Some(host) = remote_url.host.as_ref() { + host + } else { + return Ok(()); + }; + + let mut session = ssh2::Session::new().map_err(Error::Ssh)?; + session + .set_tcp_stream(std::net::TcpStream::connect(format!("{}:22", host)).map_err(Error::Io)?); + session.handshake().map_err(Error::Ssh)?; + + let mut known_hosts = session.known_hosts().map_err(Error::Ssh)?; + + // Initialize the known hosts with a global known hosts file + let dotssh = Path::new(&env::var("HOME").unwrap()).join(".ssh"); + let file = dotssh.join("known_hosts"); + if !file.exists() { + fs::create_dir_all(&dotssh).map_err(Error::Io)?; + fs::File::create(&file).map_err(Error::Io)?; + } + + known_hosts + .read_file(&file, KnownHostFileKind::OpenSSH) + .map_err(Error::Ssh)?; + + // Now check to see if the seesion's host key is anywhere in the known + // hosts file + let (key, key_type) = session.host_key().unwrap(); + match known_hosts.check(host, key) { + CheckResult::Match => Ok(()), + CheckResult::Mismatch => Err(Error::MismatchedHostKey), + CheckResult::Failure => Err(Error::Failure), + CheckResult::NotFound => { + tracing::info!("adding host key for {}", host); + known_hosts + .add(host, key, "added by gitbutler client", key_type.into()) + .map_err(Error::Ssh)?; + known_hosts + .write_file(&file, KnownHostFileKind::OpenSSH) + .map_err(Error::Ssh)?; + Ok(()) + } + } +}