From e3bf214ec69e061bfa3404bf55c162843bf8a549 Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:23:09 +0200 Subject: [PATCH] windowing and starting aes-ctr --- Cargo.lock | 2 + src/main.rs | 38 ++++- ssh-connection/src/lib.rs | 202 ++++++++++++++++++++-- ssh-transport/Cargo.toml | 2 + ssh-transport/README.md | 1 + ssh-transport/src/crypto.rs | 212 +---------------------- ssh-transport/src/crypto/encrypt.rs | 249 ++++++++++++++++++++++++++++ ssh-transport/src/lib.rs | 12 +- ssh-transport/src/numbers.rs | 2 + ssh-transport/src/packet/ctors.rs | 1 + 10 files changed, 489 insertions(+), 232 deletions(-) create mode 100644 ssh-transport/src/crypto/encrypt.rs diff --git a/Cargo.lock b/Cargo.lock index 1432bfa..f9b1a2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,9 +926,11 @@ dependencies = [ name = "ssh-transport" version = "0.1.0" dependencies = [ + "aes", "aes-gcm", "chacha20", "crypto-bigint", + "ctr", "ed25519-dalek", "eyre", "hex-literal", diff --git a/src/main.rs b/src/main.rs index 702ba79..fa26f08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,10 +45,24 @@ async fn main() -> eyre::Result<()> { let mut total_sent_data = Vec::new(); if let Err(err) = handle_connection(next, &mut total_sent_data).await { + if let Some(err) = err.downcast_ref::() { + if err.kind() == std::io::ErrorKind::ConnectionReset { + return; + } + } + error!(?err, "error handling connection"); } - info!(stdin = %String::from_utf8_lossy(&total_sent_data), "Finished connection"); + // Limit stdin to 500 characters. + let stdin = String::from_utf8_lossy(&total_sent_data); + let stdin = if let Some((idx, _)) = stdin.char_indices().nth(500) { + &stdin[..idx] + } else { + &stdin + }; + + info!(?stdin, "Finished connection"); } .instrument(span), ); @@ -111,6 +125,7 @@ async fn handle_connection( } while let Some(update) = state.next_channel_update() { + //eprintln!("{:?}", update); match update.kind { ChannelUpdateKind::Open(kind) => match kind { ChannelOpen::Session => { @@ -126,6 +141,21 @@ async fn handle_connection( } } ChannelRequest::Shell { want_reply } => { + state.do_operation( + update.number.construct_op(ChannelOperationKind::Data( + vec![b'a'; 1_000_000], + )), + ); + state.do_operation( + update.number.construct_op(ChannelOperationKind::Data( + vec![b'b'; 1_000_000], + )), + ); + state.do_operation( + update.number.construct_op(ChannelOperationKind::Data( + vec![b'c'; 1_000_000], + )), + ); if want_reply { state.do_operation(success); } @@ -177,11 +207,11 @@ async fn handle_connection( b"Thanks Hayley!".to_vec(), )), ); - state.do_operation(update.number.construct_op(ChannelOperationKind::Close)); + //state.do_operation(update.number.construct_op(ChannelOperationKind::Close)); } - if is_eof { - debug!(channel = %update.number, "Received EOF, closing channel"); + if false && is_eof { + debug!(channel = %update.number, "Received Ctrl-C, closing channel"); state.do_operation(update.number.construct_op(ChannelOperationKind::Close)); } diff --git a/ssh-connection/src/lib.rs b/ssh-connection/src/lib.rs index 1dca12b..e1ed445 100644 --- a/ssh-connection/src/lib.rs +++ b/ssh-connection/src/lib.rs @@ -1,5 +1,6 @@ +use std::cmp; use std::collections::{HashMap, VecDeque}; -use tracing::{debug, info, warn}; +use tracing::{debug, info, trace, warn}; use ssh_transport::packet::Packet; use ssh_transport::Result; @@ -28,14 +29,32 @@ struct Channel { we_closed: bool, /// The channel number for the other side. peer_channel: u32, + /// The current max window size of our peer, controls how many bytes we can still send. + peer_window_size: u32, + /// The max packet size of the peer. + // We need to split our packets if the user requests more. + peer_max_packet_size: u32, + + /// For validation only. + our_window_size: u32, + /// For validation only. + our_max_packet_size: u32, + /// By how much we want to increase the window when it gets small. + our_window_size_increase_step: u32, + + /// Queued data that we want to send, but have not been able to because of the window limits. + /// Whenever we get more window space, we will send this data. + queued_data: Vec, } /// An update from a channel. /// The receiver-equivalent of [`ChannelOperation`]. +#[derive(Debug)] pub struct ChannelUpdate { pub number: ChannelNumber, pub kind: ChannelUpdateKind, } +#[derive(Debug)] pub enum ChannelUpdateKind { Open(ChannelOpen), Request(ChannelRequest), @@ -44,11 +63,11 @@ pub enum ChannelUpdateKind { Eof, Closed, } - +#[derive(Debug)] pub enum ChannelOpen { Session, } - +#[derive(Debug)] pub enum ChannelRequest { PtyReq { want_reply: bool, @@ -168,6 +187,13 @@ impl ServerChannelsState { Channel { we_closed: false, peer_channel: sender_channel, + peer_max_packet_size: max_packet_size, + peer_window_size: initial_window_size, + our_max_packet_size: max_packet_size, + our_window_size: initial_window_size, + our_window_size_increase_step: initial_window_size, + + queued_data: Vec::new(), }, ); @@ -178,11 +204,59 @@ impl ServerChannelsState { debug!(%channel_type, %our_number, "Successfully opened channel"); } + numbers::SSH_MSG_CHANNEL_WINDOW_ADJUST => { + let our_channel = packet.u32()?; + let our_channel = self.validate_channel(our_channel)?; + let bytes_to_add = packet.u32()?; + + let channel = self.channel(our_channel)?; + channel.peer_window_size = channel + .peer_window_size + .checked_add(bytes_to_add) + .ok_or_else(|| client_error!("window size larger than 2^32"))?; + + if !channel.queued_data.is_empty() { + let limit = + cmp::min(channel.queued_data.len(), channel.peer_window_size as usize); + let data_to_send = channel.queued_data.splice(..limit, []).collect::>(); + self.send_data(our_channel, &data_to_send); + } + } numbers::SSH_MSG_CHANNEL_DATA => { let our_channel = packet.u32()?; let our_channel = self.validate_channel(our_channel)?; let data = packet.string()?; + let channel = self.channel(our_channel)?; + channel.our_window_size = channel + .our_window_size + .checked_sub(data.len() as u32) + .ok_or_else(|| { + client_error!( + "sent more data than the window allows: {} while the window is {}", + data.len(), + channel.our_window_size + ) + })?; + if channel.our_max_packet_size < (data.len() as u32) { + return Err(client_error!( + "data bigger than allowed packet size: {} while the max packet size is {}", + data.len(), + channel.our_max_packet_size + )); + } + + trace!(channel = %our_channel, window = %channel.our_window_size, "Remaining window on our side"); + + // We probably want to make this user-controllable in the future. + if channel.our_window_size < 1000 { + let peer = channel.peer_channel; + let bytes_to_add = channel.our_window_size_increase_step; + channel.our_window_size += bytes_to_add; + self.packets_to_send + .push_back(Packet::new_msg_channel_window_adjust(peer, bytes_to_add)) + } + self.channel_updates.push_back(ChannelUpdate { number: our_channel, kind: ChannelUpdateKind::Data { @@ -206,6 +280,7 @@ impl ServerChannelsState { let our_channel = self.validate_channel(our_channel)?; let channel = self.channel(our_channel)?; if !channel.we_closed { + info!("closeing here"); let close = Packet::new_msg_channel_close(channel.peer_channel); self.packets_to_send.push_back(close); } @@ -225,7 +300,7 @@ impl ServerChannelsState { let request_type = packet.utf8_string()?; let want_reply = packet.bool()?; - debug!(%our_channel, %request_type, "Got channel request"); + debug!(channel = %our_channel, %request_type, "Got channel request"); let channel = self.channel(our_channel)?; let peer_channel = channel.peer_channel; @@ -240,7 +315,7 @@ impl ServerChannelsState { let term_modes = packet.string()?; debug!( - %our_channel, + channel = %our_channel, %term, %width_chars, %height_rows, @@ -258,12 +333,12 @@ impl ServerChannelsState { } } "shell" => { - info!(%our_channel, "Opening shell"); + info!(channel = %our_channel, "Opening shell"); ChannelRequest::Shell { want_reply } } "exec" => { let command = packet.string()?; - info!(%our_channel, command = %String::from_utf8_lossy(command), "Executing command"); + info!(channel = %our_channel, command = %String::from_utf8_lossy(command), "Executing command"); ChannelRequest::Exec { want_reply, command: command.to_owned(), @@ -273,7 +348,7 @@ impl ServerChannelsState { let name = packet.utf8_string()?; let value = packet.string()?; - info!(%our_channel, %name, value = %String::from_utf8_lossy(value), "Setting environment variable"); + info!(channel = %our_channel, %name, value = %String::from_utf8_lossy(value), "Setting environment variable"); ChannelRequest::Env { want_reply, @@ -282,12 +357,12 @@ impl ServerChannelsState { } } "signal" => { - debug!(%our_channel, "Received signal"); + debug!(channel = %our_channel, "Received signal"); // Ignore signals, something we can do. return Ok(()); } _ => { - warn!(%request_type, %our_channel, "Unknown channel request"); + warn!(%request_type, channel = %our_channel, "Unknown channel request"); self.send_channel_failure(peer_channel); return Ok(()); } @@ -299,7 +374,10 @@ impl ServerChannelsState { }) } _ => { - todo!("unsupported packet: {} ({packet_type})", numbers::packet_type_to_string(packet_type).unwrap_or("")); + todo!( + "unsupported packet: {} ({packet_type})", + numbers::packet_type_to_string(packet_type).unwrap_or("") + ); } } @@ -314,17 +392,28 @@ impl ServerChannelsState { self.channel_updates.pop_front() } + /// Executes an operation on the channel. + /// + /// # Panics + /// This will panic when the channel has already been closed. pub fn do_operation(&mut self, op: ChannelOperation) { - let peer = self + op.trace(); + + let channel = self .channel(op.number) - .expect("passed channel ID that does not exist") - .peer_channel; + .expect("passed channel ID that does not exist"); + let peer = channel.peer_channel; + + if channel.we_closed { + debug!("Dropping operation as channel has been closed already"); + return; + } + match op.kind { ChannelOperationKind::Success => self.send_channel_success(peer), ChannelOperationKind::Failure => self.send_channel_failure(peer), ChannelOperationKind::Data(data) => { - self.packets_to_send - .push_back(Packet::new_msg_channel_data(peer, &data)); + self.send_data(op.number, &data); } ChannelOperationKind::Request(req) => { let packet = match req { @@ -358,6 +447,46 @@ impl ServerChannelsState { } } + fn send_data(&mut self, channel_number: ChannelNumber, data: &[u8]) { + let channel = self.channel(channel_number).unwrap(); + let peer = channel.peer_channel; + + let mut chunks = data.chunks(channel.peer_max_packet_size as usize); + + while let Some(data) = chunks.next() { + let channel = self.channel(channel_number).unwrap(); + let remaining_window_space_after = + channel.peer_window_size.checked_sub(data.len() as u32); + match remaining_window_space_after { + None => { + let rest = channel.peer_window_size; + let (to_send, to_keep) = data.split_at(rest as usize); + + // Send everything we can, which empties the window. + channel.peer_window_size -= rest; + assert_eq!(channel.peer_window_size, 0); + self.packets_to_send + .push_back(Packet::new_msg_channel_data(peer, to_send)); + + // It's over, we have exhausted all window space. + // Queue the rest of the bytes. + let channel = self.channel(channel_number).unwrap(); + channel.queued_data.extend_from_slice(to_keep); + for data in chunks { + channel.queued_data.extend_from_slice(data); + } + debug!(channel = %channel_number, queue_len = %channel.queued_data.len(), "Exhausted window space, queueing the rest of the data"); + return; + } + Some(space) => channel.peer_window_size = space, + } + trace!(channel = %channel_number, window = %channel.peer_window_size, "Remaining window on their side"); + + self.packets_to_send + .push_back(Packet::new_msg_channel_data(peer, data)); + } + } + fn send_channel_success(&mut self, recipient_channel: u32) { self.packets_to_send .push_back(Packet::new_msg_channel_success(recipient_channel)); @@ -381,3 +510,44 @@ impl ServerChannelsState { .ok_or_else(|| client_error!("unknown channel: {number:?}")) } } + +impl ChannelOperation { + /// Logs the attempted operation. + fn trace(&self) { + let kind = match &self.kind { + ChannelOperationKind::Success => "success", + ChannelOperationKind::Failure => "failure", + ChannelOperationKind::Data(_) => "data", + ChannelOperationKind::Request(req) => match req { + ChannelRequest::PtyReq { .. } => "pty-req", + ChannelRequest::Shell { .. } => "shell", + ChannelRequest::Exec { .. } => "exec", + ChannelRequest::Env { .. } => "env", + ChannelRequest::ExitStatus { .. } => "exit-status", + }, + ChannelOperationKind::Eof => "eof", + ChannelOperationKind::Close => "close", + }; + trace!(number = %self.number, %kind, "Attempt channel operation") + } +} + +#[cfg(test)] +mod tests { + use ssh_transport::packet::Packet; + + use crate::ServerChannelsState; + + #[test] + fn only_single_close_for_double_close_operation() { + let state = ServerChannelsState::new(); + //state.recv_packet(); + } + + #[test] + #[should_panic] + fn panic_when_data_operation_after_close() { + let state = ServerChannelsState::new(); + //state.recv_packet(); + } +} diff --git a/ssh-transport/Cargo.toml b/ssh-transport/Cargo.toml index dbc0bac..49bc96b 100644 --- a/ssh-transport/Cargo.toml +++ b/ssh-transport/Cargo.toml @@ -4,9 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] +aes = "0.8.4" aes-gcm = "0.10.3" chacha20 = "0.9.1" crypto-bigint = "0.5.5" +ctr = "0.9.2" ed25519-dalek = { version = "2.1.1" } eyre = "0.6.12" p256 = { version = "0.13.2", features = ["ecdh", "ecdsa"] } diff --git a/ssh-transport/README.md b/ssh-transport/README.md index 960f599..7bef03b 100644 --- a/ssh-transport/README.md +++ b/ssh-transport/README.md @@ -7,6 +7,7 @@ and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker. and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250). Other relevant RFCs: +- [RFC 4344 The Secure Shell (SSH) Transport Layer Encryption Modes](https://datatracker.ietf.org/doc/html/rfc4344) - [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647) - [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656) - [RFC 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668) diff --git a/ssh-transport/src/crypto.rs b/ssh-transport/src/crypto.rs index b25d8e8..458060e 100644 --- a/ssh-transport/src/crypto.rs +++ b/ssh-transport/src/crypto.rs @@ -1,8 +1,7 @@ -use aes_gcm::aead::AeadMutInPlace; -use chacha20::cipher::{KeyInit, StreamCipher, StreamCipherSeek}; +pub mod encrypt; + use p256::ecdsa::signature::Signer; use sha2::Digest; -use subtle::ConstantTimeEq; use crate::{ client_error, @@ -95,40 +94,6 @@ impl AlgorithmName for EncryptionAlgorithm { self.name } } -pub const ENC_CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm { - name: "chacha20-poly1305@openssh.com", - iv_size: 0, - key_size: 64, // 32 for header, 32 for main - decrypt_len: |state, bytes, packet_number| { - let alg = ChaCha20Poly1305OpenSsh::from_state(state); - alg.decrypt_len(bytes, packet_number) - }, - decrypt_packet: |state, bytes, packet_number| { - let alg = ChaCha20Poly1305OpenSsh::from_state(state); - alg.decrypt_packet(bytes, packet_number) - }, - encrypt_packet: |state, packet, packet_number| { - let alg = ChaCha20Poly1305OpenSsh::from_state(state); - alg.encrypt_packet(packet, packet_number) - }, -}; -pub const ENC_AES256_GCM: EncryptionAlgorithm = EncryptionAlgorithm { - name: "aes256-gcm@openssh.com", - iv_size: 12, - key_size: 32, - decrypt_len: |state, bytes, packet_number| { - let mut alg = Aes256GcmOpenSsh::from_state(state); - alg.decrypt_len(bytes, packet_number) - }, - decrypt_packet: |state, bytes, packet_number| { - let mut alg = Aes256GcmOpenSsh::from_state(state); - alg.decrypt_packet(bytes, packet_number) - }, - encrypt_packet: |state, packet, packet_number| { - let mut alg = Aes256GcmOpenSsh::from_state(state); - alg.encrypt_packet(packet, packet_number) - }, -}; pub struct EncodedSshPublicHostKey(pub Vec); pub struct EncodedSshSignature(pub Vec); @@ -425,176 +390,3 @@ pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8 } add_to_hash(key); } - -/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version. -type SshChaCha20 = chacha20::ChaCha20Legacy; - -/// -struct ChaCha20Poly1305OpenSsh { - header_key: chacha20::Key, - main_key: chacha20::Key, -} - -impl ChaCha20Poly1305OpenSsh { - fn from_state(keys: &[u8]) -> Self { - assert_eq!(keys.len(), 64); - Self { - main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(), - header_key: <[u8; 32]>::try_from(&keys[32..]).unwrap().into(), - } - } - - fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) { - // - let mut cipher = ::new( - &self.header_key, - &packet_number.to_be_bytes().into(), - ); - cipher.apply_keystream(bytes); - } - - fn decrypt_packet(&self, mut bytes: RawPacket, packet_number: u64) -> Result { - // - - let mut cipher = ::new( - &self.main_key, - &packet_number.to_be_bytes().into(), - ); - - let tag_offset = bytes.full_packet().len() - 16; - let authenticated = &bytes.full_packet()[..tag_offset]; - - let mac = { - let mut poly1305_key = [0; poly1305::KEY_SIZE]; - cipher.apply_keystream(&mut poly1305_key); - poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(authenticated) - }; - - let read_tag = poly1305::Tag::from_slice(&bytes.full_packet()[tag_offset..]); - - if !bool::from(mac.ct_eq(read_tag)) { - return Err(crate::client_error!( - "failed to decrypt: invalid poly1305 MAC" - )); - } - - // Advance ChaCha's block counter to 1 - cipher - .seek(::block_size()); - - let encrypted_packet_content = bytes.content_mut(); - cipher.apply_keystream(encrypted_packet_content); - - Packet::from_full(encrypted_packet_content) - } - - fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket { - let mut bytes = packet.to_bytes(false, Packet::DEFAULT_BLOCK_SIZE); - - // Prepare the main cipher. - let mut main_cipher = ::new( - &self.main_key, - &packet_number.to_be_bytes().into(), - ); - - // Get the poly1305 key first, but don't use it yet! - // We encrypt-then-mac. - let mut poly1305_key = [0; poly1305::KEY_SIZE]; - main_cipher.apply_keystream(&mut poly1305_key); - - // As the first act of encryption, encrypt the length. - let mut len_cipher = ::new( - &self.header_key, - &packet_number.to_be_bytes().into(), - ); - len_cipher.apply_keystream(&mut bytes[..4]); - - // Advance ChaCha's block counter to 1 - main_cipher - .seek(::block_size()); - // Encrypt the content of the packet, excluding the length and the MAC, which is not pushed yet. - main_cipher.apply_keystream(&mut bytes[4..]); - - // Now, MAC the length || content, and push that to the end. - let mac = poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(&bytes); - - bytes.extend_from_slice(mac.as_slice()); - - EncryptedPacket::from_encrypted_full_bytes(bytes) - } -} - -/// -/// -struct Aes256GcmOpenSsh<'a> { - key: aes_gcm::Key, - nonce: &'a mut [u8; 12], -} - -impl<'a> Aes256GcmOpenSsh<'a> { - fn from_state(keys: &'a mut [u8]) -> Self { - assert_eq!(keys.len(), 44); - Self { - key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(), - nonce: <&mut [u8; 12]>::try_from(&mut keys[32..]).unwrap(), - } - } - - fn decrypt_len(&mut self, _: &mut [u8], _: u64) { - // AES-GCM does not encrypt the length. - // - } - - fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result { - let mut cipher = aes_gcm::Aes256Gcm::new(&self.key); - - let mut len = [0; 4]; - len.copy_from_slice(&bytes.full_packet()[..4]); - - let tag_offset = bytes.full_packet().len() - 16; - let mut tag = [0; 16]; - tag.copy_from_slice(&bytes.full_packet()[tag_offset..]); - - let encrypted_packet_content = bytes.content_mut(); - - cipher - .decrypt_in_place_detached( - (&*self.nonce).into(), - &len, - encrypted_packet_content, - (&tag).into(), - ) - .map_err(|_| crate::client_error!("failed to decrypt: invalid GCM MAC"))?; - self.inc_nonce(); - - Packet::from_full(encrypted_packet_content) - } - - fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket { - let mut bytes = packet.to_bytes( - false, - ::block_size() as u8, - ); - - let mut cipher = aes_gcm::Aes256Gcm::new(&self.key); - - let (aad, plaintext) = bytes.split_at_mut(4); - - let tag = cipher - .encrypt_in_place_detached((&*self.nonce).into(), aad, plaintext) - .unwrap(); - bytes.extend_from_slice(&tag); - self.inc_nonce(); - - EncryptedPacket::from_encrypted_full_bytes(bytes) - } - - fn inc_nonce(&mut self) { - let mut carry = 1; - for i in (0..self.nonce.len()).rev() { - let n = self.nonce[i] as u16 + carry; - self.nonce[i] = n as u8; - carry = n >> 8; - } - } -} diff --git a/ssh-transport/src/crypto/encrypt.rs b/ssh-transport/src/crypto/encrypt.rs new file mode 100644 index 0000000..2196b87 --- /dev/null +++ b/ssh-transport/src/crypto/encrypt.rs @@ -0,0 +1,249 @@ +use crate::Result; +use aes_gcm::{aead::AeadMutInPlace, KeyInit}; +use chacha20::cipher::{StreamCipher, StreamCipherSeek}; +use subtle::ConstantTimeEq; + +use crate::packet::{EncryptedPacket, Packet, RawPacket}; + +use super::EncryptionAlgorithm; + +pub const CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm { + name: "chacha20-poly1305@openssh.com", + iv_size: 0, + key_size: 64, // 32 for header, 32 for main + decrypt_len: |state, bytes, packet_number| { + let alg = ChaCha20Poly1305OpenSsh::from_state(state); + alg.decrypt_len(bytes, packet_number) + }, + decrypt_packet: |state, bytes, packet_number| { + let alg = ChaCha20Poly1305OpenSsh::from_state(state); + alg.decrypt_packet(bytes, packet_number) + }, + encrypt_packet: |state, packet, packet_number| { + let alg = ChaCha20Poly1305OpenSsh::from_state(state); + alg.encrypt_packet(packet, packet_number) + }, +}; +pub const AES256_GCM: EncryptionAlgorithm = EncryptionAlgorithm { + name: "aes256-gcm@openssh.com", + iv_size: 12, + key_size: 32, + decrypt_len: |state, bytes, packet_number| { + let mut alg = Aes256GcmOpenSsh::from_state(state); + alg.decrypt_len(bytes, packet_number) + }, + decrypt_packet: |state, bytes, packet_number| { + let mut alg = Aes256GcmOpenSsh::from_state(state); + alg.decrypt_packet(bytes, packet_number) + }, + encrypt_packet: |state, packet, packet_number| { + let mut alg = Aes256GcmOpenSsh::from_state(state); + alg.encrypt_packet(packet, packet_number) + }, +}; +/// RFC 4344 AES128 in counter mode. +/// +pub const ENC_AES128_CTR: EncryptionAlgorithm = EncryptionAlgorithm { + name: "aes128-ctr", + iv_size: 12, + key_size: 32, + decrypt_len: |state, bytes, packet_number| { + let mut alg = Aes128Ctr::from_state(state); + alg.decrypt_len(bytes, packet_number) + }, + decrypt_packet: |state, bytes, packet_number| todo!(), + encrypt_packet: |state, packet, packet_number| todo!(), +}; + +/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version. +type SshChaCha20 = chacha20::ChaCha20Legacy; + +/// +struct ChaCha20Poly1305OpenSsh { + header_key: chacha20::Key, + main_key: chacha20::Key, +} + +impl ChaCha20Poly1305OpenSsh { + fn from_state(keys: &[u8]) -> Self { + assert_eq!(keys.len(), 64); + Self { + main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(), + header_key: <[u8; 32]>::try_from(&keys[32..]).unwrap().into(), + } + } + + fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) { + // + let mut cipher = ::new( + &self.header_key, + &packet_number.to_be_bytes().into(), + ); + cipher.apply_keystream(bytes); + } + + fn decrypt_packet(&self, mut bytes: RawPacket, packet_number: u64) -> Result { + // + + let mut cipher = ::new( + &self.main_key, + &packet_number.to_be_bytes().into(), + ); + + let tag_offset = bytes.full_packet().len() - 16; + let authenticated = &bytes.full_packet()[..tag_offset]; + + let mac = { + let mut poly1305_key = [0; poly1305::KEY_SIZE]; + cipher.apply_keystream(&mut poly1305_key); + poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(authenticated) + }; + + let read_tag = poly1305::Tag::from_slice(&bytes.full_packet()[tag_offset..]); + + if !bool::from(mac.ct_eq(read_tag)) { + return Err(crate::client_error!( + "failed to decrypt: invalid poly1305 MAC" + )); + } + + // Advance ChaCha's block counter to 1 + cipher + .seek(::block_size()); + + let encrypted_packet_content = bytes.content_mut(); + cipher.apply_keystream(encrypted_packet_content); + + Packet::from_full(encrypted_packet_content) + } + + fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket { + let mut bytes = packet.to_bytes(false, Packet::DEFAULT_BLOCK_SIZE); + + // Prepare the main cipher. + let mut main_cipher = ::new( + &self.main_key, + &packet_number.to_be_bytes().into(), + ); + + // Get the poly1305 key first, but don't use it yet! + // We encrypt-then-mac. + let mut poly1305_key = [0; poly1305::KEY_SIZE]; + main_cipher.apply_keystream(&mut poly1305_key); + + // As the first act of encryption, encrypt the length. + let mut len_cipher = ::new( + &self.header_key, + &packet_number.to_be_bytes().into(), + ); + len_cipher.apply_keystream(&mut bytes[..4]); + + // Advance ChaCha's block counter to 1 + main_cipher + .seek(::block_size()); + // Encrypt the content of the packet, excluding the length and the MAC, which is not pushed yet. + main_cipher.apply_keystream(&mut bytes[4..]); + + // Now, MAC the length || content, and push that to the end. + let mac = poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(&bytes); + + bytes.extend_from_slice(mac.as_slice()); + + EncryptedPacket::from_encrypted_full_bytes(bytes) + } +} + +/// +/// +struct Aes256GcmOpenSsh<'a> { + key: aes_gcm::Key, + nonce: &'a mut [u8; 12], +} + +impl<'a> Aes256GcmOpenSsh<'a> { + fn from_state(keys: &'a mut [u8]) -> Self { + assert_eq!(keys.len(), 44); + Self { + key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(), + nonce: <&mut [u8; 12]>::try_from(&mut keys[32..]).unwrap(), + } + } + + fn decrypt_len(&mut self, _: &mut [u8], _: u64) { + // AES-GCM does not encrypt the length. + // + } + + fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result { + let mut cipher = aes_gcm::Aes256Gcm::new(&self.key); + + let mut len = [0; 4]; + len.copy_from_slice(&bytes.full_packet()[..4]); + + let tag_offset = bytes.full_packet().len() - 16; + let mut tag = [0; 16]; + tag.copy_from_slice(&bytes.full_packet()[tag_offset..]); + + let encrypted_packet_content = bytes.content_mut(); + + cipher + .decrypt_in_place_detached( + (&*self.nonce).into(), + &len, + encrypted_packet_content, + (&tag).into(), + ) + .map_err(|_| crate::client_error!("failed to decrypt: invalid GCM MAC"))?; + self.inc_nonce(); + + Packet::from_full(encrypted_packet_content) + } + + fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket { + let mut bytes = packet.to_bytes( + false, + ::block_size() as u8, + ); + + let mut cipher = aes_gcm::Aes256Gcm::new(&self.key); + + let (aad, plaintext) = bytes.split_at_mut(4); + + let tag = cipher + .encrypt_in_place_detached((&*self.nonce).into(), aad, plaintext) + .unwrap(); + bytes.extend_from_slice(&tag); + self.inc_nonce(); + + EncryptedPacket::from_encrypted_full_bytes(bytes) + } + + fn inc_nonce(&mut self) { + let mut carry = 1; + for i in (0..self.nonce.len()).rev() { + let n = self.nonce[i] as u16 + carry; + self.nonce[i] = n as u8; + carry = n >> 8; + } + } +} + +struct Aes128Ctr { + key: ctr::Ctr128BE, +} +impl Aes128Ctr { + fn from_state(keys: &mut [u8]) -> Self { + todo!() + } + + fn decrypt_len(&mut self, _: &mut [u8], _: u64) { + + } + + fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result { + todo!() + } + fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket { + todo!() + } +} diff --git a/ssh-transport/src/lib.rs b/ssh-transport/src/lib.rs index cbebfe2..a01ea04 100644 --- a/ssh-transport/src/lib.rs +++ b/ssh-transport/src/lib.rs @@ -205,10 +205,18 @@ impl ServerConnection { debug!(name = %server_host_key_algorithm.name(), "Using host key algorithm"); let encryption_algorithms_client_to_server = AlgorithmNegotiation { - supported: vec![crypto::ENC_CHACHA20POLY1305, crypto::ENC_AES256_GCM], + supported: vec![ + crypto::encrypt::CHACHA20POLY1305, + crypto::encrypt::AES256_GCM, + // crypto::encrypt::ENC_AES128_CTR, + ], }; let encryption_algorithms_server_to_client = AlgorithmNegotiation { - supported: vec![crypto::ENC_CHACHA20POLY1305, crypto::ENC_AES256_GCM], + supported: vec![ + crypto::encrypt::CHACHA20POLY1305, + crypto::encrypt::AES256_GCM, + // crypto::encrypt::ENC_AES128_CTR, + ], }; let encryption_client_to_server = encryption_algorithms_client_to_server diff --git a/ssh-transport/src/numbers.rs b/ssh-transport/src/numbers.rs index 7c0a16d..bdfcdd3 100644 --- a/ssh-transport/src/numbers.rs +++ b/ssh-transport/src/numbers.rs @@ -64,6 +64,8 @@ pub fn packet_type_to_string(packet_type: u8) -> Option<&'static str> { 6 => "SSH_MSG_SERVICE_ACCEPT", 20 => "SSH_MSG_KEXINIT", 21 => "SSH_MSG_NEWKEYS", + 30 => "SSH_MSG_KEX_ECDH_INIT", + 31 => "SSH_MSG_KEX_ECDH_REPLY", 50 => "SSH_MSG_USERAUTH_REQUEST", 51 => "SSH_MSG_USERAUTH_FAILURE", 52 => "SSH_MSG_USERAUTH_SUCCESS", diff --git a/ssh-transport/src/packet/ctors.rs b/ssh-transport/src/packet/ctors.rs index 3d0bb55..fc29b25 100644 --- a/ssh-transport/src/packet/ctors.rs +++ b/ssh-transport/src/packet/ctors.rs @@ -90,6 +90,7 @@ ctors! { description: string, language_tag: string, ); + fn new_msg_channel_window_adjust(SSH_MSG_CHANNEL_WINDOW_ADJUST; recipient_channel: u32, bytes_to_add: u32); fn new_msg_channel_data(SSH_MSG_CHANNEL_DATA; recipient_channel: u32, data: string); fn new_msg_channel_eof(SSH_MSG_CHANNEL_EOF; recipient_channel: u32);