Skip to main content

wireforge_app/ssh/
kex.rs

1//! SSH binary packet and key exchange init parser (RFC 4253 §6, §7.1).
2
3pub struct SshBinaryPacket<'a> { buf: &'a [u8], payload_start: usize, payload_len: usize }
4
5impl<'a> SshBinaryPacket<'a> {
6    pub fn parse(buf: &'a [u8]) -> Option<Self> {
7        if buf.len() < 5 { return None; }
8        let packet_len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
9        let padding_len = buf[4] as usize;
10        let payload_len = packet_len.saturating_sub(padding_len).saturating_sub(1);
11        Some(Self { buf, payload_start: 5, payload_len })
12    }
13
14    pub fn packet_length(&self) -> u32 { u32::from_be_bytes([self.buf[0], self.buf[1], self.buf[2], self.buf[3]]) }
15    pub fn padding_length(&self) -> u8 { self.buf[4] }
16    pub fn payload(&self) -> &'a [u8] {
17        let end = (self.payload_start + self.payload_len).min(self.buf.len());
18        &self.buf[self.payload_start..end]
19    }
20}
21
22#[derive(Debug, Clone)]
23pub struct SshKexInit<'a> { payload: &'a [u8], cursor: usize }
24
25impl<'a> SshKexInit<'a> {
26    pub fn parse(buf: &'a [u8]) -> Option<Self> {
27        let pkt = SshBinaryPacket::parse(buf)?;
28        let payload = pkt.payload();
29        if payload.is_empty() || payload[0] != 20 { return None; }
30        if payload.len() < 17 { return None; }
31        Some(Self { payload, cursor: 17 }) // skip type(1) + cookie(16)
32    }
33
34    pub fn cookie(&self) -> &[u8; 16] {
35        self.payload[1..17].try_into().unwrap() // parse() guarantees payload.len() >= 17
36    }
37
38    fn next_name_list(&mut self) -> Vec<String> {
39        let data = &self.payload[self.cursor..];
40        if data.len() < 4 { return vec![]; }
41        let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
42        self.cursor += 4;
43        let end = (self.cursor + len).min(self.payload.len());
44        let list_bytes = &self.payload[self.cursor..end];
45        self.cursor = end;
46        if list_bytes.is_empty() { return vec![]; }
47        std::str::from_utf8(list_bytes).unwrap_or("").split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
48    }
49
50    pub fn kex_algorithms(&mut self) -> Vec<String> { self.next_name_list() }
51    pub fn server_host_key_algorithms(&mut self) -> Vec<String> { self.next_name_list() }
52    pub fn encryption_algorithms_cts(&mut self) -> Vec<String> { self.next_name_list() }
53    pub fn encryption_algorithms_stc(&mut self) -> Vec<String> { self.next_name_list() }
54    pub fn mac_algorithms_cts(&mut self) -> Vec<String> { self.next_name_list() }
55    pub fn mac_algorithms_stc(&mut self) -> Vec<String> { self.next_name_list() }
56    pub fn compression_algorithms_cts(&mut self) -> Vec<String> { self.next_name_list() }
57    pub fn compression_algorithms_stc(&mut self) -> Vec<String> { self.next_name_list() }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn parse_kex_init() {
66        let cookie = [0xAAu8; 16];
67        let name_list = b"curve25519-sha256";
68        let nl_len = name_list.len() as u32;
69        let mut payload = vec![20u8];
70        payload.extend_from_slice(&cookie);
71        for _ in 0..8 {
72            payload.extend_from_slice(&nl_len.to_be_bytes());
73            payload.extend_from_slice(name_list);
74        }
75        payload.push(0);
76        payload.extend_from_slice(&[0u8; 4]);
77
78        let packet_len = 1 + payload.len() + 4; // padding_len + payload + padding
79        let mut pkt = vec![];
80        pkt.extend_from_slice(&(packet_len as u32).to_be_bytes());
81        pkt.push(4); // padding_length
82        pkt.extend_from_slice(&payload);
83        pkt.extend_from_slice(&[0u8; 4]); // padding
84
85        let mut kex = SshKexInit::parse(&pkt).unwrap();
86        assert_eq!(kex.cookie(), &cookie);
87        assert_eq!(kex.kex_algorithms(), vec!["curve25519-sha256".to_string()]);
88    }
89}