diff --git a/Cargo.lock b/Cargo.lock index ef4dad6..c7f6446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,7 +97,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -107,7 +107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -620,7 +620,7 @@ dependencies = [ "hermit-abi", "libc", "wasi", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -692,7 +692,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -872,6 +872,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1009,7 +1030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1029,6 +1050,7 @@ dependencies = [ "clap", "eyre", "rand", + "rpassword", "ssh-protocol", "ssh-transport", "tokio", @@ -1122,7 +1144,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1278,13 +1300,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1293,28 +1339,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1327,24 +1391,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/ssh-connection/src/lib.rs b/ssh-connection/src/lib.rs index 30652a7..70409b1 100644 --- a/ssh-connection/src/lib.rs +++ b/ssh-connection/src/lib.rs @@ -16,12 +16,14 @@ impl std::fmt::Display for ChannelNumber { } } -pub struct ServerChannelsState { +pub struct ChannelsState { packets_to_send: VecDeque, channel_updates: VecDeque, channels: HashMap, next_channel_id: ChannelNumber, + + is_server: bool, } struct Channel { @@ -121,13 +123,15 @@ pub enum ChannelOperationKind { Close, } -impl ServerChannelsState { - pub fn new() -> Self { - ServerChannelsState { +impl ChannelsState { + pub fn new(is_server: bool) -> Self { + ChannelsState { packets_to_send: VecDeque::new(), channels: HashMap::new(), channel_updates: VecDeque::new(), next_channel_id: ChannelNumber(0), + + is_server, } } @@ -549,7 +553,7 @@ impl ChannelOperation { mod tests { use ssh_transport::{numbers, packet::Packet}; - use crate::{ChannelNumber, ChannelOperation, ChannelOperationKind, ServerChannelsState}; + use crate::{ChannelNumber, ChannelOperation, ChannelOperationKind, ChannelsState}; /// If a test fails, add this to the test to get logs. #[allow(dead_code)] @@ -560,7 +564,7 @@ mod tests { } #[track_caller] - fn assert_response_types(state: &mut ServerChannelsState, types: &[u8]) { + fn assert_response_types(state: &mut ChannelsState, types: &[u8]) { let response = state .packets_to_send() .map(|p| numbers::packet_type_to_string(p.packet_type())) @@ -573,7 +577,7 @@ mod tests { assert_eq!(expected, response); } - fn open_session_channel(state: &mut ServerChannelsState) { + fn open_session_channel(state: &mut ChannelsState) { state .recv_packet(Packet::new_msg_channel_open_session( b"session", 0, 2048, 1024, @@ -584,7 +588,7 @@ mod tests { #[test] fn interactive_pty() { - let state = &mut ServerChannelsState::new(); + let state = &mut ChannelsState::new(true); open_session_channel(state); state @@ -615,7 +619,7 @@ mod tests { #[test] fn only_single_close_for_double_close_operation() { - let state = &mut ServerChannelsState::new(); + let state = &mut ChannelsState::new(true); open_session_channel(state); state.do_operation(ChannelOperation { number: ChannelNumber(0), @@ -630,7 +634,7 @@ mod tests { #[test] fn ignore_operation_after_close() { - let mut state = &mut ServerChannelsState::new(); + let mut state = &mut ChannelsState::new(true); open_session_channel(state); state.recv_packet(Packet::new_msg_channel_close(0)).unwrap(); assert_response_types(&mut state, &[numbers::SSH_MSG_CHANNEL_CLOSE]); @@ -643,7 +647,7 @@ mod tests { #[test] fn respect_peer_windowing() { - let state = &mut ServerChannelsState::new(); + let state = &mut ChannelsState::new(true); state .recv_packet(Packet::new_msg_channel_open_session(b"session", 0, 10, 50)) .unwrap(); @@ -684,7 +688,7 @@ mod tests { #[test] fn send_windowing_adjustments() { - let state = &mut ServerChannelsState::new(); + let state = &mut ChannelsState::new(true); state .recv_packet(Packet::new_msg_channel_open_session( b"session", 0, 2000, 2000, diff --git a/ssh-protocol/src/lib.rs b/ssh-protocol/src/lib.rs index 944e4c8..130fb59 100644 --- a/ssh-protocol/src/lib.rs +++ b/ssh-protocol/src/lib.rs @@ -1,8 +1,11 @@ +use std::mem; + pub use ssh_connection as connection; use ssh_connection::ChannelOperation; pub use ssh_connection::{ChannelUpdate, ChannelUpdateKind}; pub use ssh_transport as transport; pub use ssh_transport::{Result, SshStatus}; +use tracing::debug; pub struct ServerConnection { transport: ssh_transport::server::ServerConnection, @@ -11,7 +14,7 @@ pub struct ServerConnection { enum ServerConnectionState { Auth(auth::BadAuth), - Open(ssh_connection::ServerChannelsState), + Open(ssh_connection::ChannelsState), } impl ServerConnection { @@ -34,16 +37,15 @@ impl ServerConnection { } if auth.is_authenticated() { self.state = - ServerConnectionState::Open(ssh_connection::ServerChannelsState::new()); + ServerConnectionState::Open(ssh_connection::ChannelsState::new(true)); } } ServerConnectionState::Open(con) => { con.recv_packet(packet)?; - for to_send in con.packets_to_send() { - self.transport.send_plaintext_packet(to_send); - } } } + + self.progress(); } Ok(()) @@ -65,6 +67,125 @@ impl ServerConnection { ServerConnectionState::Auth(_) => panic!("tried to get connection during auth"), ServerConnectionState::Open(con) => { con.do_operation(op); + self.progress(); + } + } + } + + pub fn progress(&mut self) { + match &mut self.state { + ServerConnectionState::Auth(auth) => { + for to_send in auth.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + } + ServerConnectionState::Open(con) => { + for to_send in con.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + } + } + } +} + +pub struct ClientConnection { + transport: ssh_transport::client::ClientConnection, + state: ClientConnectionState, +} + +enum ClientConnectionState { + Setup(Option), + Auth(auth::ClientAuth), + Open(ssh_connection::ChannelsState), +} + +impl ClientConnection { + pub fn new(transport: ssh_transport::client::ClientConnection, auth: auth::ClientAuth) -> Self { + Self { + transport, + state: ClientConnectionState::Setup(Some(auth)), + } + } + + pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> { + self.transport.recv_bytes(bytes)?; + + if let ClientConnectionState::Setup(auth) = &mut self.state { + if self.transport.is_open() { + let mut auth = mem::take(auth).unwrap(); + for to_send in auth.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + debug!("Connection has been opened"); + self.state = ClientConnectionState::Auth(auth); + } + } + + while let Some(packet) = self.transport.next_plaintext_packet() { + match &mut self.state { + ClientConnectionState::Setup(_) => unreachable!("handled above"), + ClientConnectionState::Auth(auth) => { + auth.recv_packet(packet)?; + for to_send in auth.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + if auth.is_authenticated() { + self.state = + ClientConnectionState::Open(ssh_connection::ChannelsState::new(false)); + } + } + ClientConnectionState::Open(con) => { + con.recv_packet(packet)?; + for to_send in con.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + } + } + } + + Ok(()) + } + + pub fn auth(&mut self) -> Option<&mut auth::ClientAuth> { + match &mut self.state { + ClientConnectionState::Auth(auth) => Some(auth), + _ => None, + } + } + + pub fn next_msg_to_send(&mut self) -> Option { + self.transport.next_msg_to_send() + } + + pub fn next_channel_update(&mut self) -> Option { + match &mut self.state { + ClientConnectionState::Setup(_) => None, + ClientConnectionState::Auth(_) => None, + ClientConnectionState::Open(con) => con.next_channel_update(), + } + } + + pub fn do_operation(&mut self, op: ChannelOperation) { + match &mut self.state { + ClientConnectionState::Setup(_) | ClientConnectionState::Auth(_) => { + panic!("tried to get connection during auth") + } + ClientConnectionState::Open(con) => { + con.do_operation(op); + self.progress(); + } + } + } + + pub fn progress(&mut self) { + match &mut self.state { + ClientConnectionState::Setup(_) => {} + ClientConnectionState::Auth(auth) => { + for to_send in auth.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + } + ClientConnectionState::Open(con) => { for to_send in con.packets_to_send() { self.transport.send_plaintext_packet(to_send); } @@ -77,7 +198,7 @@ impl ServerConnection { pub mod auth { use std::collections::VecDeque; - use ssh_transport::{peer_error, numbers, packet::Packet, parse::NameList, Result}; + use ssh_transport::{numbers, packet::Packet, parse::NameList, peer_error, Result}; use tracing::info; pub struct BadAuth { @@ -183,4 +304,95 @@ pub mod auth { self.packets_to_send.push_back(packet); } } + + pub struct ClientAuth { + username: Vec, + packets_to_send: VecDeque, + user_requests: VecDeque, + is_authenticated: bool, + } + + pub enum ClientUserRequest { + Password, + Banner(Vec), + } + + impl ClientAuth { + pub fn new(username: Vec) -> Self { + let mut packets_to_send = VecDeque::new(); + let initial_useruath_req = + Packet::new_msg_userauth_request_none(&username, b"ssh-connection", b"none"); + packets_to_send.push_back(initial_useruath_req); + + Self { + packets_to_send, + username, + user_requests: VecDeque::new(), + is_authenticated: false, + } + } + + pub fn is_authenticated(&self) -> bool { + self.is_authenticated + } + + pub fn packets_to_send(&mut self) -> impl Iterator + '_ { + self.packets_to_send.drain(..) + } + + pub fn user_requests(&mut self) -> impl Iterator + '_ { + self.user_requests.drain(..) + } + + pub fn send_password(&mut self, password: &str) { + let packet = Packet::new_msg_userauth_request_password( + &self.username, + b"ssh-connection", + b"password", + false, + password.as_bytes(), + ); + self.packets_to_send.push_back(packet); + } + + pub fn recv_packet(&mut self, packet: Packet) -> Result<()> { + assert!(!self.is_authenticated, "Must not feed more packets to authentication after authentication is been completed, check with .is_authenticated()"); + + let mut p = packet.payload_parser(); + let packet_type = p.u8()?; + + match packet_type { + numbers::SSH_MSG_USERAUTH_BANNER => { + let banner = p.string()?; + let _lang = p.string()?; + + self.user_requests + .push_back(ClientUserRequest::Banner(banner.to_vec())); + } + numbers::SSH_MSG_USERAUTH_FAILURE => { + let authentications = p.name_list()?; + let _partial_success = p.bool()?; + + if authentications.iter().any(|item| item == "password") { + self.user_requests.push_back(ClientUserRequest::Password); + } else { + return Err(peer_error!( + "server does not support password authentication" + )); + } + } + numbers::SSH_MSG_USERAUTH_SUCCESS => { + self.is_authenticated = true; + } + _ => { + return Err(peer_error!( + "unexpected packet: {}", + numbers::packet_type_to_string(packet_type) + )) + } + } + + Ok(()) + } + } } diff --git a/ssh-transport/src/client.rs b/ssh-transport/src/client.rs index 78553be..dd4e6d4 100644 --- a/ssh-transport/src/client.rs +++ b/ssh-transport/src/client.rs @@ -304,6 +304,18 @@ impl ClientConnection { self.packet_transport.next_msg_to_send() } + pub fn next_plaintext_packet(&mut self) -> Option { + self.plaintext_packets.pop_front() + } + + pub fn send_plaintext_packet(&mut self, packet: Packet) { + self.packet_transport.queue_packet(packet); + } + + pub fn is_open(&self) -> bool { + matches!(self.state, ClientState::Open) + } + fn send_kexinit(&mut self, client_ident: Vec, server_ident: Vec) { let mut cookie = [0; 16]; self.rng.fill_bytes(&mut cookie); diff --git a/ssh-transport/src/packet/ctors.rs b/ssh-transport/src/packet/ctors.rs index 217d7a6..9f7803a 100644 --- a/ssh-transport/src/packet/ctors.rs +++ b/ssh-transport/src/packet/ctors.rs @@ -63,6 +63,18 @@ ctors! { // User authentication protocol: // 50 to 59 User authentication generic + fn new_msg_userauth_request_none(SSH_MSG_USERAUTH_REQUEST; + username: string, + service_name: string, + method_name_none: string, + ); + fn new_msg_userauth_request_password(SSH_MSG_USERAUTH_REQUEST; + username: string, + service_name: string, + method_name_password: string, + false_: bool, + password: string, + ); fn new_msg_userauth_failure(SSH_MSG_USERAUTH_FAILURE; auth_options: name_list, partial_success: bool, diff --git a/ssh/Cargo.toml b/ssh/Cargo.toml index 7cd7db1..3e21b29 100644 --- a/ssh/Cargo.toml +++ b/ssh/Cargo.toml @@ -13,3 +13,4 @@ tokio = { version = "1.39.2", features = ["full"] } tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } tracing.workspace = true +rpassword = "7.3.1" diff --git a/ssh/src/main.rs b/ssh/src/main.rs index 40e7b25..e894ae6 100644 --- a/ssh/src/main.rs +++ b/ssh/src/main.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use clap::Parser; use eyre::Context; @@ -6,7 +8,7 @@ use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, }; -use tracing::info; +use tracing::{debug, error, info}; use ssh_protocol::{ transport::{self}, @@ -29,6 +31,10 @@ struct Args { command: Vec, } +enum Operation { + PasswordEntered(std::io::Result), +} + #[tokio::main] async fn main() -> eyre::Result<()> { let args = Args::parse(); @@ -40,7 +46,14 @@ async fn main() -> eyre::Result<()> { .await .wrap_err("connecting")?; - let mut state = transport::client::ClientConnection::new(ThreadRngRand); + let username = "hans-peter"; + + let mut state = ssh_protocol::ClientConnection::new( + transport::client::ClientConnection::new(ThreadRngRand), + ssh_protocol::auth::ClientAuth::new(username.as_bytes().to_vec()), + ); + + let (send_op, mut recv_op) = tokio::sync::mpsc::channel::(10); let mut buf = [0; 1024]; @@ -51,26 +64,62 @@ async fn main() -> eyre::Result<()> { .wrap_err("writing response")?; } - let read = conn - .read(&mut buf) - .await - .wrap_err("reading from connection")?; - if read == 0 { - info!("Did not read any bytes from TCP stream, EOF"); - return Ok(()); + if let Some(auth) = state.auth() { + for req in auth.user_requests() { + match req { + ssh_protocol::auth::ClientUserRequest::Password => { + let username = username.to_owned(); + let destination = args.destination.clone(); + let send_op = send_op.clone(); + std::thread::spawn(move || { + let password = rpassword::prompt_password(format!( + "{}@{}'s password: ", + username, destination + )); + let _ = send_op.blocking_send(Operation::PasswordEntered(password)); + }); + } + ssh_protocol::auth::ClientUserRequest::Banner(banner) => { + let banner = String::from_utf8_lossy(&banner); + std::io::stdout().write(&banner.as_bytes())?; + } + } + } } - if let Err(err) = state.recv_bytes(&buf[..read]) { - match err { - SshStatus::PeerError(err) => { - info!(?err, "disconnecting client after invalid operation"); + tokio::select! { + read = conn.read(&mut buf) => { + let read = read.wrap_err("reading from connection")?; + if read == 0 { + info!("Did not read any bytes from TCP stream, EOF"); return Ok(()); } - SshStatus::Disconnect => { - info!("Received disconnect from client"); - return Ok(()); + if let Err(err) = state.recv_bytes(&buf[..read]) { + match err { + SshStatus::PeerError(err) => { + error!(?err, "disconnecting client after invalid operation"); + return Ok(()); + } + SshStatus::Disconnect => { + error!("Received disconnect from server"); + return Ok(()); + } + } } } + op = recv_op.recv() => { + match op { + Some(Operation::PasswordEntered(password)) => { + if let Some(auth) = state.auth() { + auth.send_password(&password?); + } else { + debug!("Ignoring entered password as the state has moved on"); + } + } + None => {} + } + state.progress(); + } } } }