⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/net/
ssh.rs

1//! SSH-2.0 Server Implementation (RFC 4253, RFC 4252, RFC 4254)
2//!
3//! Provides an SSH server for VeridianOS with:
4//! - SSH-2.0 binary packet protocol and version exchange
5//! - Key exchange via curve25519-sha256 (ECDH)
6//! - Password and Ed25519 public key authentication
7//! - Channel multiplexing with flow control (window adjust)
8//! - PTY allocation, shell/exec sessions, env passing
9//! - Session state machine from version exchange through disconnect
10
11#![allow(dead_code)]
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16#[cfg(feature = "alloc")]
17use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
18
19// ============================================================================
20// SSH Message Type Constants (RFC 4253 / 4252 / 4254)
21// ============================================================================
22
23pub const SSH_MSG_DISCONNECT: u8 = 1;
24pub const SSH_MSG_IGNORE: u8 = 2;
25pub const SSH_MSG_UNIMPLEMENTED: u8 = 3;
26pub const SSH_MSG_DEBUG: u8 = 4;
27pub const SSH_MSG_SERVICE_REQUEST: u8 = 5;
28pub const SSH_MSG_SERVICE_ACCEPT: u8 = 6;
29
30pub const SSH_MSG_KEXINIT: u8 = 20;
31pub const SSH_MSG_NEWKEYS: u8 = 21;
32
33pub const SSH_MSG_KEX_ECDH_INIT: u8 = 30;
34pub const SSH_MSG_KEX_ECDH_REPLY: u8 = 31;
35
36pub const SSH_MSG_USERAUTH_REQUEST: u8 = 50;
37pub const SSH_MSG_USERAUTH_FAILURE: u8 = 51;
38pub const SSH_MSG_USERAUTH_SUCCESS: u8 = 52;
39pub const SSH_MSG_USERAUTH_BANNER: u8 = 53;
40pub const SSH_MSG_USERAUTH_PK_OK: u8 = 60;
41
42pub const SSH_MSG_GLOBAL_REQUEST: u8 = 80;
43pub const SSH_MSG_REQUEST_SUCCESS: u8 = 81;
44pub const SSH_MSG_REQUEST_FAILURE: u8 = 82;
45
46pub const SSH_MSG_CHANNEL_OPEN: u8 = 90;
47pub const SSH_MSG_CHANNEL_OPEN_CONFIRMATION: u8 = 91;
48pub const SSH_MSG_CHANNEL_OPEN_FAILURE: u8 = 92;
49pub const SSH_MSG_CHANNEL_WINDOW_ADJUST: u8 = 93;
50pub const SSH_MSG_CHANNEL_DATA: u8 = 94;
51pub const SSH_MSG_CHANNEL_EXTENDED_DATA: u8 = 95;
52pub const SSH_MSG_CHANNEL_EOF: u8 = 96;
53pub const SSH_MSG_CHANNEL_CLOSE: u8 = 97;
54pub const SSH_MSG_CHANNEL_REQUEST: u8 = 98;
55pub const SSH_MSG_CHANNEL_SUCCESS: u8 = 99;
56pub const SSH_MSG_CHANNEL_FAILURE: u8 = 100;
57
58// ============================================================================
59// SSH Disconnect Reason Codes (RFC 4253 Section 11.1)
60// ============================================================================
61
62pub const SSH_DISCONNECT_HOST_NOT_ALLOWED: u32 = 1;
63pub const SSH_DISCONNECT_PROTOCOL_ERROR: u32 = 2;
64pub const SSH_DISCONNECT_KEY_EXCHANGE_FAILED: u32 = 3;
65pub const SSH_DISCONNECT_RESERVED: u32 = 4;
66pub const SSH_DISCONNECT_MAC_ERROR: u32 = 5;
67pub const SSH_DISCONNECT_COMPRESSION_ERROR: u32 = 6;
68pub const SSH_DISCONNECT_SERVICE_NOT_AVAILABLE: u32 = 7;
69pub const SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED: u32 = 8;
70pub const SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE: u32 = 9;
71pub const SSH_DISCONNECT_CONNECTION_LOST: u32 = 10;
72pub const SSH_DISCONNECT_BY_APPLICATION: u32 = 11;
73pub const SSH_DISCONNECT_TOO_MANY_CONNECTIONS: u32 = 12;
74pub const SSH_DISCONNECT_AUTH_CANCELLED_BY_USER: u32 = 13;
75pub const SSH_DISCONNECT_NO_MORE_AUTH_METHODS: u32 = 14;
76pub const SSH_DISCONNECT_ILLEGAL_USER_NAME: u32 = 15;
77
78// ============================================================================
79// SSH Channel Open Failure Codes (RFC 4254 Section 5.1)
80// ============================================================================
81
82pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u32 = 1;
83pub const SSH_OPEN_CONNECT_FAILED: u32 = 2;
84pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u32 = 3;
85pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4;
86
87// ============================================================================
88// Protocol Constants
89// ============================================================================
90
91/// SSH-2.0 server identification string
92pub const SSH_VERSION_STRING: &[u8] = b"SSH-2.0-VeridianOS_1.0\r\n";
93
94/// Maximum SSH packet payload size (256 KB)
95const MAX_PACKET_SIZE: usize = 262144;
96
97/// Minimum padding length
98const MIN_PADDING: usize = 4;
99
100/// Block size for unencrypted packets
101const BLOCK_SIZE_CLEAR: usize = 8;
102
103/// Default initial channel window size (2 MB)
104const DEFAULT_WINDOW_SIZE: u32 = 2 * 1024 * 1024;
105
106/// Maximum channel packet data size (32 KB)
107const MAX_CHANNEL_DATA_SIZE: u32 = 32768;
108
109/// Default SSH listen port
110pub const SSH_DEFAULT_PORT: u16 = 22;
111
112/// Maximum concurrent sessions per server
113const MAX_SESSIONS: usize = 64;
114
115/// Maximum authentication attempts before disconnect
116const MAX_AUTH_ATTEMPTS: u32 = 6;
117
118/// Key length for curve25519
119const CURVE25519_KEY_LEN: usize = 32;
120
121/// Ed25519 signature length
122const ED25519_SIG_LEN: usize = 64;
123
124/// Ed25519 public key length
125const ED25519_PUB_LEN: usize = 32;
126
127/// Maximum version string length (RFC 4253)
128const MAX_VERSION_LEN: usize = 255;
129
130/// HMAC-SHA256 tag length
131const MAC_LEN: usize = 32;
132
133// ============================================================================
134// Section 1: SSH Transport Layer (~300 lines)
135// ============================================================================
136
137/// SSH error types
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum SshError {
140    /// Invalid or malformed packet
141    InvalidPacket,
142    /// Version exchange failed
143    VersionMismatch,
144    /// Key exchange failure
145    KeyExchangeFailed,
146    /// Authentication failed
147    AuthenticationFailed,
148    /// Maximum auth attempts exceeded
149    TooManyAuthAttempts,
150    /// Channel not found
151    ChannelNotFound,
152    /// Channel already exists
153    ChannelExists,
154    /// Window exhausted (flow control)
155    WindowExhausted,
156    /// Buffer too small
157    BufferTooSmall,
158    /// Connection closed
159    ConnectionClosed,
160    /// Invalid state transition
161    InvalidState,
162    /// Service not available
163    ServiceNotAvailable,
164    /// Packet too large
165    PacketTooLarge,
166    /// Invalid MAC
167    MacVerifyFailed,
168    /// Protocol error
169    ProtocolError,
170    /// Resource shortage
171    ResourceShortage,
172    /// Invalid channel type
173    InvalidChannelType,
174    /// PTY allocation failed
175    PtyAllocationFailed,
176    /// Session limit reached
177    SessionLimitReached,
178}
179
180/// SSH server session state machine
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum SessionState {
183    /// Initial state: waiting for client version string
184    VersionExchange,
185    /// Key exchange in progress
186    KeyExchange,
187    /// Key exchange complete, waiting for NEWKEYS
188    NewKeysExpected,
189    /// Authenticated session, user interaction
190    Authentication,
191    /// Fully connected and channels active
192    Connected,
193    /// Session being torn down
194    Disconnected,
195}
196
197/// Algorithms negotiated during key exchange
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct NegotiatedAlgorithms {
200    pub kex_algorithm: AlgorithmId,
201    pub host_key_algorithm: AlgorithmId,
202    pub encryption_c2s: AlgorithmId,
203    pub encryption_s2c: AlgorithmId,
204    pub mac_c2s: AlgorithmId,
205    pub mac_s2c: AlgorithmId,
206    pub compression_c2s: AlgorithmId,
207    pub compression_s2c: AlgorithmId,
208}
209
210impl Default for NegotiatedAlgorithms {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl NegotiatedAlgorithms {
217    pub fn new() -> Self {
218        Self {
219            kex_algorithm: AlgorithmId::Curve25519Sha256,
220            host_key_algorithm: AlgorithmId::SshEd25519,
221            encryption_c2s: AlgorithmId::Chacha20Poly1305,
222            encryption_s2c: AlgorithmId::Chacha20Poly1305,
223            mac_c2s: AlgorithmId::HmacSha256,
224            mac_s2c: AlgorithmId::HmacSha256,
225            compression_c2s: AlgorithmId::None,
226            compression_s2c: AlgorithmId::None,
227        }
228    }
229}
230
231/// Supported algorithm identifiers
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum AlgorithmId {
234    Curve25519Sha256,
235    SshEd25519,
236    Chacha20Poly1305,
237    Aes256Ctr,
238    HmacSha256,
239    None,
240}
241
242impl AlgorithmId {
243    /// Convert algorithm to its SSH wire name
244    pub(crate) fn as_str(&self) -> &'static str {
245        match self {
246            Self::Curve25519Sha256 => "curve25519-sha256",
247            Self::SshEd25519 => "ssh-ed25519",
248            Self::Chacha20Poly1305 => "chacha20-poly1305@openssh.com",
249            Self::Aes256Ctr => "aes256-ctr",
250            Self::HmacSha256 => "hmac-sha256",
251            Self::None => "none",
252        }
253    }
254
255    /// Parse from SSH wire name
256    pub(crate) fn parse_name(s: &str) -> Option<Self> {
257        match s {
258            "curve25519-sha256" | "curve25519-sha256@libssh.org" => Some(Self::Curve25519Sha256),
259            "ssh-ed25519" => Some(Self::SshEd25519),
260            "chacha20-poly1305@openssh.com" => Some(Self::Chacha20Poly1305),
261            "aes256-ctr" => Some(Self::Aes256Ctr),
262            "hmac-sha256" => Some(Self::HmacSha256),
263            "none" => Some(Self::None),
264            _ => None,
265        }
266    }
267
268    /// Map this algorithm to a `CipherSuite` from the shared crypto module.
269    ///
270    /// Returns `None` for algorithms that are not AEAD cipher suites
271    /// (key exchange, host key, MAC, compression) or for AES-CTR which
272    /// is not yet available as a `CipherSuite` variant.
273    pub(crate) fn as_cipher_suite(&self) -> Option<crate::crypto::cipher_suite::CipherSuite> {
274        match self {
275            Self::Chacha20Poly1305 => {
276                Some(crate::crypto::cipher_suite::CipherSuite::ChaCha20Poly1305)
277            }
278            Self::Aes256Ctr => None, // AES-CTR is not an AEAD cipher suite
279            _ => None,
280        }
281    }
282}
283
284/// SSH version information parsed from identification string
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct VersionInfo {
287    /// Protocol version (must be "2.0")
288    pub protocol_version: [u8; 3],
289    /// Software version string
290    pub software_version: Vec<u8>,
291    /// Optional comment
292    pub comment: Vec<u8>,
293}
294
295impl VersionInfo {
296    /// Parse an SSH identification string (e.g., "SSH-2.0-OpenSSH_9.0\r\n")
297    pub(crate) fn parse(data: &[u8]) -> Option<Self> {
298        // Must start with "SSH-"
299        if data.len() < 8 || &data[..4] != b"SSH-" {
300            return None;
301        }
302
303        // Find end (strip \r\n or \n)
304        let end = data.iter().position(|&b| b == b'\n').unwrap_or(data.len());
305        let line = if end > 0 && data[end - 1] == b'\r' {
306            &data[..end - 1]
307        } else {
308            &data[..end]
309        };
310
311        if line.len() > MAX_VERSION_LEN {
312            return None;
313        }
314
315        // Protocol version: "2.0" required
316        if line.len() < 7 || &line[4..7] != b"2.0" {
317            return None;
318        }
319
320        let protocol_version = [b'2', b'.', b'0'];
321
322        // Expect dash after version
323        if line.len() < 8 || line[7] != b'-' {
324            return None;
325        }
326
327        // Software version extends to space or end
328        let sw_start = 8;
329        let sw_end = line[sw_start..]
330            .iter()
331            .position(|&b| b == b' ')
332            .map(|p| sw_start + p)
333            .unwrap_or(line.len());
334
335        let software_version = line[sw_start..sw_end].to_vec();
336
337        let comment = if sw_end < line.len() {
338            line[sw_end + 1..].to_vec()
339        } else {
340            Vec::new()
341        };
342
343        Some(Self {
344            protocol_version,
345            software_version,
346            comment,
347        })
348    }
349
350    /// Encode to wire format (with trailing \r\n)
351    pub(crate) fn encode(&self) -> Vec<u8> {
352        let mut buf = Vec::with_capacity(64);
353        buf.extend_from_slice(b"SSH-");
354        buf.extend_from_slice(&self.protocol_version);
355        buf.push(b'-');
356        buf.extend_from_slice(&self.software_version);
357        if !self.comment.is_empty() {
358            buf.push(b' ');
359            buf.extend_from_slice(&self.comment);
360        }
361        buf.extend_from_slice(b"\r\n");
362        buf
363    }
364}
365
366/// SSH binary packet (RFC 4253 Section 6)
367///
368/// Wire format:
369///   packet_length (u32) || padding_length (u8) || payload || random_padding ||
370/// MAC
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct SshPacket {
373    /// Message payload bytes
374    pub payload: Vec<u8>,
375    /// Random padding (at least 4 bytes, total packet multiple of block size)
376    pub padding: Vec<u8>,
377}
378
379impl SshPacket {
380    /// Create a new packet from payload
381    pub fn new(payload: Vec<u8>) -> Self {
382        let padding = Self::compute_padding(payload.len(), BLOCK_SIZE_CLEAR);
383        Self { payload, padding }
384    }
385
386    /// Compute required padding for the given payload and block size
387    fn compute_padding(payload_len: usize, block_size: usize) -> Vec<u8> {
388        // packet_length (4) + padding_length (1) + payload + padding must be multiple
389        // of block_size padding must be at least MIN_PADDING bytes
390        let unpadded = 1 + payload_len; // padding_length byte + payload
391        let remainder = (4 + unpadded) % block_size;
392        let mut pad_len = if remainder == 0 {
393            0
394        } else {
395            block_size - remainder
396        };
397        if pad_len < MIN_PADDING {
398            pad_len += block_size;
399        }
400        // Use deterministic padding for reproducibility (real impl would use random)
401        vec![0u8; pad_len]
402    }
403
404    /// Encode packet to wire format (without MAC)
405    pub(crate) fn encode(&self) -> Vec<u8> {
406        let padding_length = self.padding.len() as u8;
407        let packet_length = (1 + self.payload.len() + self.padding.len()) as u32;
408        let total = 4 + packet_length as usize;
409
410        let mut buf = Vec::with_capacity(total);
411        buf.extend_from_slice(&packet_length.to_be_bytes());
412        buf.push(padding_length);
413        buf.extend_from_slice(&self.payload);
414        buf.extend_from_slice(&self.padding);
415        buf
416    }
417
418    /// Decode packet from wire format (without MAC verification)
419    pub(crate) fn decode(data: &[u8]) -> Result<(Self, usize), SshError> {
420        if data.len() < 5 {
421            return Err(SshError::InvalidPacket);
422        }
423
424        let packet_length = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
425
426        if !(2..=MAX_PACKET_SIZE).contains(&packet_length) {
427            return Err(SshError::PacketTooLarge);
428        }
429
430        let total = 4 + packet_length;
431        if data.len() < total {
432            return Err(SshError::InvalidPacket);
433        }
434
435        let padding_length = data[4] as usize;
436        if padding_length < MIN_PADDING || padding_length >= packet_length {
437            return Err(SshError::InvalidPacket);
438        }
439
440        let payload_length = packet_length - 1 - padding_length;
441        let payload = data[5..5 + payload_length].to_vec();
442        let padding = data[5 + payload_length..5 + payload_length + padding_length].to_vec();
443
444        Ok((Self { payload, padding }, total))
445    }
446
447    /// Get the message type byte (first byte of payload)
448    pub(crate) fn message_type(&self) -> Option<u8> {
449        self.payload.first().copied()
450    }
451}
452
453/// KEXINIT message (SSH_MSG_KEXINIT, type 20)
454#[derive(Debug, Clone, PartialEq, Eq)]
455pub struct KexInitMessage {
456    /// Random 16-byte cookie
457    pub cookie: [u8; 16],
458    /// Kex algorithms
459    pub kex_algorithms: Vec<u8>,
460    /// Server host key algorithms
461    pub server_host_key_algorithms: Vec<u8>,
462    /// Encryption algorithms client-to-server
463    pub encryption_algorithms_c2s: Vec<u8>,
464    /// Encryption algorithms server-to-client
465    pub encryption_algorithms_s2c: Vec<u8>,
466    /// MAC algorithms client-to-server
467    pub mac_algorithms_c2s: Vec<u8>,
468    /// MAC algorithms server-to-client
469    pub mac_algorithms_s2c: Vec<u8>,
470    /// Compression algorithms client-to-server
471    pub compression_algorithms_c2s: Vec<u8>,
472    /// Compression algorithms server-to-client
473    pub compression_algorithms_s2c: Vec<u8>,
474    /// Languages client-to-server
475    pub languages_c2s: Vec<u8>,
476    /// Languages server-to-client
477    pub languages_s2c: Vec<u8>,
478    /// First kex packet follows
479    pub first_kex_packet_follows: bool,
480}
481
482impl KexInitMessage {
483    /// Create a server KEXINIT with default supported algorithms
484    pub fn new_server(cookie: [u8; 16]) -> Self {
485        Self {
486            cookie,
487            kex_algorithms: Self::encode_name_list(&["curve25519-sha256"]),
488            server_host_key_algorithms: Self::encode_name_list(&["ssh-ed25519"]),
489            encryption_algorithms_c2s: Self::encode_name_list(&[
490                "chacha20-poly1305@openssh.com",
491                "aes256-ctr",
492            ]),
493            encryption_algorithms_s2c: Self::encode_name_list(&[
494                "chacha20-poly1305@openssh.com",
495                "aes256-ctr",
496            ]),
497            mac_algorithms_c2s: Self::encode_name_list(&["hmac-sha256"]),
498            mac_algorithms_s2c: Self::encode_name_list(&["hmac-sha256"]),
499            compression_algorithms_c2s: Self::encode_name_list(&["none"]),
500            compression_algorithms_s2c: Self::encode_name_list(&["none"]),
501            languages_c2s: Self::encode_name_list(&[]),
502            languages_s2c: Self::encode_name_list(&[]),
503            first_kex_packet_follows: false,
504        }
505    }
506
507    /// Encode a name-list as SSH wire bytes (comma-separated, length-prefixed)
508    fn encode_name_list(names: &[&str]) -> Vec<u8> {
509        let mut joined = Vec::new();
510        for (i, name) in names.iter().enumerate() {
511            if i > 0 {
512                joined.push(b',');
513            }
514            joined.extend_from_slice(name.as_bytes());
515        }
516        joined
517    }
518
519    /// Encode KEXINIT message to payload bytes (including msg type byte)
520    pub(crate) fn encode(&self) -> Vec<u8> {
521        let mut buf = Vec::with_capacity(512);
522        buf.push(SSH_MSG_KEXINIT);
523        buf.extend_from_slice(&self.cookie);
524
525        // Helper to write a name-list with u32 length prefix
526        fn write_name_list(buf: &mut Vec<u8>, data: &[u8]) {
527            buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
528            buf.extend_from_slice(data);
529        }
530
531        write_name_list(&mut buf, &self.kex_algorithms);
532        write_name_list(&mut buf, &self.server_host_key_algorithms);
533        write_name_list(&mut buf, &self.encryption_algorithms_c2s);
534        write_name_list(&mut buf, &self.encryption_algorithms_s2c);
535        write_name_list(&mut buf, &self.mac_algorithms_c2s);
536        write_name_list(&mut buf, &self.mac_algorithms_s2c);
537        write_name_list(&mut buf, &self.compression_algorithms_c2s);
538        write_name_list(&mut buf, &self.compression_algorithms_s2c);
539        write_name_list(&mut buf, &self.languages_c2s);
540        write_name_list(&mut buf, &self.languages_s2c);
541
542        buf.push(if self.first_kex_packet_follows { 1 } else { 0 });
543        // Reserved u32
544        buf.extend_from_slice(&0u32.to_be_bytes());
545
546        buf
547    }
548
549    /// Decode KEXINIT from payload bytes (after msg type byte is consumed)
550    pub(crate) fn decode(data: &[u8]) -> Result<Self, SshError> {
551        if data.len() < 17 {
552            return Err(SshError::InvalidPacket);
553        }
554
555        // First byte should be SSH_MSG_KEXINIT
556        if data[0] != SSH_MSG_KEXINIT {
557            return Err(SshError::InvalidPacket);
558        }
559
560        let mut cookie = [0u8; 16];
561        cookie.copy_from_slice(&data[1..17]);
562
563        let mut pos = 17;
564
565        fn read_name_list(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, SshError> {
566            if *pos + 4 > data.len() {
567                return Err(SshError::InvalidPacket);
568            }
569            let len =
570                u32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]])
571                    as usize;
572            *pos += 4;
573            if *pos + len > data.len() {
574                return Err(SshError::InvalidPacket);
575            }
576            let result = data[*pos..*pos + len].to_vec();
577            *pos += len;
578            Ok(result)
579        }
580
581        let kex_algorithms = read_name_list(data, &mut pos)?;
582        let server_host_key_algorithms = read_name_list(data, &mut pos)?;
583        let encryption_algorithms_c2s = read_name_list(data, &mut pos)?;
584        let encryption_algorithms_s2c = read_name_list(data, &mut pos)?;
585        let mac_algorithms_c2s = read_name_list(data, &mut pos)?;
586        let mac_algorithms_s2c = read_name_list(data, &mut pos)?;
587        let compression_algorithms_c2s = read_name_list(data, &mut pos)?;
588        let compression_algorithms_s2c = read_name_list(data, &mut pos)?;
589        let languages_c2s = read_name_list(data, &mut pos)?;
590        let languages_s2c = read_name_list(data, &mut pos)?;
591
592        let first_kex_packet_follows = if pos < data.len() {
593            data[pos] != 0
594        } else {
595            false
596        };
597
598        Ok(Self {
599            cookie,
600            kex_algorithms,
601            server_host_key_algorithms,
602            encryption_algorithms_c2s,
603            encryption_algorithms_s2c,
604            mac_algorithms_c2s,
605            mac_algorithms_s2c,
606            compression_algorithms_c2s,
607            compression_algorithms_s2c,
608            languages_c2s,
609            languages_s2c,
610            first_kex_packet_follows,
611        })
612    }
613}
614
615/// Key exchange state for curve25519-sha256
616#[derive(Debug, Clone, PartialEq, Eq)]
617pub struct KexState {
618    /// Our (server) ephemeral private key
619    pub server_ephemeral_private: [u8; CURVE25519_KEY_LEN],
620    /// Our (server) ephemeral public key
621    pub server_ephemeral_public: [u8; CURVE25519_KEY_LEN],
622    /// Client ephemeral public key (from SSH_MSG_KEX_ECDH_INIT)
623    pub client_ephemeral_public: [u8; CURVE25519_KEY_LEN],
624    /// Shared secret K
625    pub shared_secret: [u8; CURVE25519_KEY_LEN],
626    /// Exchange hash H (session ID on first exchange)
627    pub exchange_hash: [u8; 32],
628    /// Whether key exchange is complete
629    pub complete: bool,
630}
631
632impl Default for KexState {
633    fn default() -> Self {
634        Self::new()
635    }
636}
637
638impl KexState {
639    pub fn new() -> Self {
640        Self {
641            server_ephemeral_private: [0u8; CURVE25519_KEY_LEN],
642            server_ephemeral_public: [0u8; CURVE25519_KEY_LEN],
643            client_ephemeral_public: [0u8; CURVE25519_KEY_LEN],
644            shared_secret: [0u8; CURVE25519_KEY_LEN],
645            exchange_hash: [0u8; 32],
646            complete: false,
647        }
648    }
649
650    /// Set the server ephemeral key pair
651    pub(crate) fn set_server_keys(&mut self, private: [u8; 32], public: [u8; 32]) {
652        self.server_ephemeral_private = private;
653        self.server_ephemeral_public = public;
654    }
655
656    /// Set the client's ephemeral public key from KEX_ECDH_INIT
657    pub(crate) fn set_client_public(&mut self, key: [u8; 32]) {
658        self.client_ephemeral_public = key;
659    }
660}
661
662/// SSH transport encryption keys derived from key exchange
663#[derive(Debug, Clone, PartialEq, Eq)]
664pub struct TransportKeys {
665    /// Encryption key client-to-server
666    pub enc_key_c2s: [u8; 32],
667    /// Encryption key server-to-client
668    pub enc_key_s2c: [u8; 32],
669    /// Integrity key client-to-server
670    pub mac_key_c2s: [u8; 32],
671    /// Integrity key server-to-client
672    pub mac_key_s2c: [u8; 32],
673    /// IV client-to-server
674    pub iv_c2s: [u8; 12],
675    /// IV server-to-client
676    pub iv_s2c: [u8; 12],
677}
678
679impl Default for TransportKeys {
680    fn default() -> Self {
681        Self::new()
682    }
683}
684
685impl TransportKeys {
686    pub fn new() -> Self {
687        Self {
688            enc_key_c2s: [0u8; 32],
689            enc_key_s2c: [0u8; 32],
690            mac_key_c2s: [0u8; 32],
691            mac_key_s2c: [0u8; 32],
692            iv_c2s: [0u8; 12],
693            iv_s2c: [0u8; 12],
694        }
695    }
696}
697
698/// Encode a disconnect message
699pub(crate) fn encode_disconnect(reason_code: u32, description: &str) -> Vec<u8> {
700    let desc_bytes = description.as_bytes();
701    let mut buf = Vec::with_capacity(16 + desc_bytes.len());
702    buf.push(SSH_MSG_DISCONNECT);
703    buf.extend_from_slice(&reason_code.to_be_bytes());
704    // description string (length-prefixed)
705    buf.extend_from_slice(&(desc_bytes.len() as u32).to_be_bytes());
706    buf.extend_from_slice(desc_bytes);
707    // language tag (empty)
708    buf.extend_from_slice(&0u32.to_be_bytes());
709    buf
710}
711
712/// Encode a NEWKEYS message
713pub(crate) fn encode_newkeys() -> Vec<u8> {
714    vec![SSH_MSG_NEWKEYS]
715}
716
717/// Encode a service accept message
718pub(crate) fn encode_service_accept(service_name: &str) -> Vec<u8> {
719    let name_bytes = service_name.as_bytes();
720    let mut buf = Vec::with_capacity(5 + name_bytes.len());
721    buf.push(SSH_MSG_SERVICE_ACCEPT);
722    buf.extend_from_slice(&(name_bytes.len() as u32).to_be_bytes());
723    buf.extend_from_slice(name_bytes);
724    buf
725}
726
727/// Parse a service request, return service name
728pub(crate) fn parse_service_request(payload: &[u8]) -> Result<Vec<u8>, SshError> {
729    if payload.is_empty() || payload[0] != SSH_MSG_SERVICE_REQUEST {
730        return Err(SshError::InvalidPacket);
731    }
732    if payload.len() < 5 {
733        return Err(SshError::InvalidPacket);
734    }
735    let len = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]) as usize;
736    if payload.len() < 5 + len {
737        return Err(SshError::InvalidPacket);
738    }
739    Ok(payload[5..5 + len].to_vec())
740}
741
742// ============================================================================
743// Section 2: Authentication (~250 lines)
744// ============================================================================
745
746/// Authentication method
747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
748pub enum AuthMethod {
749    /// No authentication
750    None,
751    /// Password authentication
752    Password,
753    /// Public key authentication
754    PublicKey,
755    /// Keyboard-interactive
756    KeyboardInteractive,
757}
758
759impl AuthMethod {
760    pub(crate) fn as_str(&self) -> &'static str {
761        match self {
762            Self::None => "none",
763            Self::Password => "password",
764            Self::PublicKey => "publickey",
765            Self::KeyboardInteractive => "keyboard-interactive",
766        }
767    }
768
769    pub(crate) fn from_bytes(data: &[u8]) -> Option<Self> {
770        match data {
771            b"none" => Some(Self::None),
772            b"password" => Some(Self::Password),
773            b"publickey" => Some(Self::PublicKey),
774            b"keyboard-interactive" => Some(Self::KeyboardInteractive),
775            _ => None,
776        }
777    }
778}
779
780/// Authentication state tracking
781#[derive(Debug, Clone, PartialEq, Eq)]
782pub struct AuthState {
783    /// Username being authenticated
784    pub username: Vec<u8>,
785    /// Number of failed attempts
786    pub attempts: u32,
787    /// Maximum allowed attempts
788    pub max_attempts: u32,
789    /// Authenticated successfully
790    pub authenticated: bool,
791    /// Service name requested
792    pub service_name: Vec<u8>,
793    /// Allowed methods
794    pub allowed_methods: Vec<AuthMethod>,
795    /// Partial success flag
796    pub partial_success: bool,
797}
798
799impl Default for AuthState {
800    fn default() -> Self {
801        Self::new()
802    }
803}
804
805impl AuthState {
806    pub fn new() -> Self {
807        Self {
808            username: Vec::new(),
809            attempts: 0,
810            max_attempts: MAX_AUTH_ATTEMPTS,
811            authenticated: false,
812            service_name: Vec::new(),
813            allowed_methods: vec![AuthMethod::Password, AuthMethod::PublicKey],
814            partial_success: false,
815        }
816    }
817
818    /// Record a failed attempt, return whether more attempts are allowed
819    pub(crate) fn record_failure(&mut self) -> bool {
820        self.attempts += 1;
821        self.attempts < self.max_attempts
822    }
823
824    /// Mark authentication as successful
825    pub(crate) fn mark_success(&mut self) {
826        self.authenticated = true;
827    }
828
829    /// Check if authentication attempts are exhausted
830    pub(crate) fn exhausted(&self) -> bool {
831        self.attempts >= self.max_attempts
832    }
833}
834
835/// Parsed userauth request
836#[derive(Debug, Clone, PartialEq, Eq)]
837pub struct UserauthRequest {
838    /// Username
839    pub username: Vec<u8>,
840    /// Service name
841    pub service_name: Vec<u8>,
842    /// Authentication method
843    pub method: AuthMethod,
844    /// Method-specific data
845    pub method_data: AuthMethodData,
846}
847
848/// Method-specific authentication data
849#[derive(Debug, Clone, PartialEq, Eq)]
850pub enum AuthMethodData {
851    /// No additional data
852    None,
853    /// Password authentication data
854    Password {
855        /// The password
856        password: Vec<u8>,
857    },
858    /// Public key authentication data
859    PublicKey {
860        /// Whether this is a real auth (true) or a query (false)
861        has_signature: bool,
862        /// Algorithm name (e.g., "ssh-ed25519")
863        algorithm: Vec<u8>,
864        /// Public key blob
865        public_key: Vec<u8>,
866        /// Signature (only if has_signature is true)
867        signature: Vec<u8>,
868    },
869}
870
871/// Parse a userauth request from payload
872pub(crate) fn parse_userauth_request(payload: &[u8]) -> Result<UserauthRequest, SshError> {
873    if payload.is_empty() || payload[0] != SSH_MSG_USERAUTH_REQUEST {
874        return Err(SshError::InvalidPacket);
875    }
876
877    let mut pos = 1;
878
879    // Read username
880    let username = read_ssh_string(payload, &mut pos)?;
881    // Read service name
882    let service_name = read_ssh_string(payload, &mut pos)?;
883    // Read method name
884    let method_name = read_ssh_string(payload, &mut pos)?;
885
886    let method = AuthMethod::from_bytes(&method_name).ok_or(SshError::InvalidPacket)?;
887
888    let method_data = match method {
889        AuthMethod::None => AuthMethodData::None,
890        AuthMethod::Password => {
891            // boolean: FALSE (no password change)
892            if pos >= payload.len() {
893                return Err(SshError::InvalidPacket);
894            }
895            let _change_password = payload[pos];
896            pos += 1;
897            let password = read_ssh_string(payload, &mut pos)?;
898            AuthMethodData::Password { password }
899        }
900        AuthMethod::PublicKey => {
901            if pos >= payload.len() {
902                return Err(SshError::InvalidPacket);
903            }
904            let has_signature = payload[pos] != 0;
905            pos += 1;
906            let algorithm = read_ssh_string(payload, &mut pos)?;
907            let public_key = read_ssh_string(payload, &mut pos)?;
908            let signature = if has_signature {
909                read_ssh_string(payload, &mut pos)?
910            } else {
911                Vec::new()
912            };
913            AuthMethodData::PublicKey {
914                has_signature,
915                algorithm,
916                public_key,
917                signature,
918            }
919        }
920        AuthMethod::KeyboardInteractive => AuthMethodData::None,
921    };
922
923    Ok(UserauthRequest {
924        username,
925        service_name,
926        method,
927        method_data,
928    })
929}
930
931/// Encode a userauth failure message
932pub(crate) fn encode_userauth_failure(methods: &[AuthMethod], partial_success: bool) -> Vec<u8> {
933    let mut name_list = Vec::new();
934    for (i, method) in methods.iter().enumerate() {
935        if i > 0 {
936            name_list.push(b',');
937        }
938        name_list.extend_from_slice(method.as_str().as_bytes());
939    }
940
941    let mut buf = Vec::with_capacity(8 + name_list.len());
942    buf.push(SSH_MSG_USERAUTH_FAILURE);
943    buf.extend_from_slice(&(name_list.len() as u32).to_be_bytes());
944    buf.extend_from_slice(&name_list);
945    buf.push(if partial_success { 1 } else { 0 });
946    buf
947}
948
949/// Encode a userauth success message
950pub(crate) fn encode_userauth_success() -> Vec<u8> {
951    vec![SSH_MSG_USERAUTH_SUCCESS]
952}
953
954/// Encode a userauth PK_OK (server accepts this key for auth)
955pub(crate) fn encode_userauth_pk_ok(algorithm: &[u8], public_key: &[u8]) -> Vec<u8> {
956    let mut buf = Vec::with_capacity(9 + algorithm.len() + public_key.len());
957    buf.push(SSH_MSG_USERAUTH_PK_OK);
958    write_ssh_string(&mut buf, algorithm);
959    write_ssh_string(&mut buf, public_key);
960    buf
961}
962
963/// Encode a banner message
964pub(crate) fn encode_banner(message: &str) -> Vec<u8> {
965    let msg_bytes = message.as_bytes();
966    let mut buf = Vec::with_capacity(9 + msg_bytes.len());
967    buf.push(SSH_MSG_USERAUTH_BANNER);
968    write_ssh_string(&mut buf, msg_bytes);
969    // language tag (empty)
970    write_ssh_string(&mut buf, b"");
971    buf
972}
973
974// ============================================================================
975// Section 3: Channel Management (~300 lines)
976// ============================================================================
977
978/// Channel type
979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
980pub enum ChannelType {
981    /// Interactive session
982    Session,
983    /// Direct TCP/IP forwarding
984    DirectTcpip,
985    /// Forwarded TCP/IP
986    ForwardedTcpip,
987}
988
989impl ChannelType {
990    pub(crate) fn as_str(&self) -> &'static str {
991        match self {
992            Self::Session => "session",
993            Self::DirectTcpip => "direct-tcpip",
994            Self::ForwardedTcpip => "forwarded-tcpip",
995        }
996    }
997
998    pub(crate) fn from_bytes(data: &[u8]) -> Option<Self> {
999        match data {
1000            b"session" => Some(Self::Session),
1001            b"direct-tcpip" => Some(Self::DirectTcpip),
1002            b"forwarded-tcpip" => Some(Self::ForwardedTcpip),
1003            _ => None,
1004        }
1005    }
1006}
1007
1008/// Channel state
1009#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1010pub enum ChannelState {
1011    /// Channel open request sent/received
1012    Opening,
1013    /// Channel confirmed and active
1014    Open,
1015    /// EOF sent on this channel
1016    EofSent,
1017    /// EOF received on this channel
1018    EofReceived,
1019    /// Close sent
1020    CloseSent,
1021    /// Close received
1022    CloseReceived,
1023    /// Fully closed
1024    Closed,
1025}
1026
1027/// SSH channel
1028#[derive(Debug, Clone, PartialEq, Eq)]
1029pub struct Channel {
1030    /// Local channel ID
1031    pub local_id: u32,
1032    /// Remote (peer) channel ID
1033    pub remote_id: u32,
1034    /// Channel type
1035    pub channel_type: ChannelType,
1036    /// Current state
1037    pub state: ChannelState,
1038    /// Local window size remaining (how much data we can receive)
1039    pub local_window: u32,
1040    /// Remote window size remaining (how much data we can send)
1041    pub remote_window: u32,
1042    /// Local maximum packet size
1043    pub local_max_packet: u32,
1044    /// Remote maximum packet size
1045    pub remote_max_packet: u32,
1046    /// Whether a PTY has been allocated
1047    pub pty_allocated: bool,
1048    /// Associated PTY info (if allocated)
1049    pub pty_info: Option<PtyInfo>,
1050    /// Whether a shell or exec has been started
1051    pub session_started: bool,
1052    /// Environment variables set via "env" requests
1053    pub env_vars: Vec<(Vec<u8>, Vec<u8>)>,
1054    /// EOF received from remote
1055    pub eof_received: bool,
1056    /// EOF sent to remote
1057    pub eof_sent: bool,
1058}
1059
1060impl Channel {
1061    /// Create a new channel in the Opening state
1062    pub fn new(local_id: u32, channel_type: ChannelType) -> Self {
1063        Self {
1064            local_id,
1065            remote_id: 0,
1066            channel_type,
1067            state: ChannelState::Opening,
1068            local_window: DEFAULT_WINDOW_SIZE,
1069            remote_window: 0,
1070            local_max_packet: MAX_CHANNEL_DATA_SIZE,
1071            remote_max_packet: 0,
1072            pty_allocated: false,
1073            pty_info: None,
1074            session_started: false,
1075            env_vars: Vec::new(),
1076            eof_received: false,
1077            eof_sent: false,
1078        }
1079    }
1080
1081    /// Confirm channel open from remote
1082    pub(crate) fn confirm(&mut self, remote_id: u32, remote_window: u32, remote_max_packet: u32) {
1083        self.remote_id = remote_id;
1084        self.remote_window = remote_window;
1085        self.remote_max_packet = remote_max_packet;
1086        self.state = ChannelState::Open;
1087    }
1088
1089    /// Consume send window (returns true if enough window available)
1090    pub(crate) fn consume_send_window(&mut self, amount: u32) -> bool {
1091        if self.remote_window >= amount {
1092            self.remote_window -= amount;
1093            true
1094        } else {
1095            false
1096        }
1097    }
1098
1099    /// Consume receive window
1100    pub(crate) fn consume_recv_window(&mut self, amount: u32) -> bool {
1101        if self.local_window >= amount {
1102            self.local_window -= amount;
1103            true
1104        } else {
1105            false
1106        }
1107    }
1108
1109    /// Adjust local window (increase capacity to receive more data)
1110    pub(crate) fn adjust_local_window(&mut self, increment: u32) {
1111        self.local_window = self.local_window.saturating_add(increment);
1112    }
1113
1114    /// Adjust remote window (peer sent WINDOW_ADJUST)
1115    pub(crate) fn adjust_remote_window(&mut self, increment: u32) {
1116        self.remote_window = self.remote_window.saturating_add(increment);
1117    }
1118
1119    /// Mark EOF received
1120    pub(crate) fn mark_eof_received(&mut self) {
1121        self.eof_received = true;
1122        if self.state == ChannelState::Open {
1123            self.state = ChannelState::EofReceived;
1124        }
1125    }
1126
1127    /// Mark EOF sent
1128    pub(crate) fn mark_eof_sent(&mut self) {
1129        self.eof_sent = true;
1130        if self.state == ChannelState::Open {
1131            self.state = ChannelState::EofSent;
1132        }
1133    }
1134
1135    /// Mark channel as closed
1136    pub(crate) fn close(&mut self) {
1137        self.state = ChannelState::Closed;
1138    }
1139}
1140
1141/// Channel table managing multiple channels
1142#[derive(Debug, Clone, PartialEq, Eq)]
1143pub struct ChannelTable {
1144    /// Channels indexed by local ID
1145    channels: BTreeMap<u32, Channel>,
1146    /// Next local channel ID to allocate
1147    next_id: u32,
1148}
1149
1150impl Default for ChannelTable {
1151    fn default() -> Self {
1152        Self::new()
1153    }
1154}
1155
1156impl ChannelTable {
1157    pub fn new() -> Self {
1158        Self {
1159            channels: BTreeMap::new(),
1160            next_id: 0,
1161        }
1162    }
1163
1164    /// Allocate a new channel, returning its local ID
1165    pub(crate) fn open(&mut self, channel_type: ChannelType) -> Result<u32, SshError> {
1166        let id = self.next_id;
1167        self.next_id = self.next_id.wrapping_add(1);
1168        let channel = Channel::new(id, channel_type);
1169        self.channels.insert(id, channel);
1170        Ok(id)
1171    }
1172
1173    /// Get a channel by local ID
1174    pub(crate) fn get(&self, local_id: u32) -> Option<&Channel> {
1175        self.channels.get(&local_id)
1176    }
1177
1178    /// Get a mutable channel by local ID
1179    pub(crate) fn get_mut(&mut self, local_id: u32) -> Option<&mut Channel> {
1180        self.channels.get_mut(&local_id)
1181    }
1182
1183    /// Remove a channel
1184    pub(crate) fn remove(&mut self, local_id: u32) -> Option<Channel> {
1185        self.channels.remove(&local_id)
1186    }
1187
1188    /// Number of active channels
1189    pub(crate) fn count(&self) -> usize {
1190        self.channels.len()
1191    }
1192
1193    /// Check if a channel exists
1194    pub(crate) fn contains(&self, local_id: u32) -> bool {
1195        self.channels.contains_key(&local_id)
1196    }
1197}
1198
1199/// Encode a channel open message
1200pub(crate) fn encode_channel_open(
1201    channel_type: ChannelType,
1202    sender_channel: u32,
1203    initial_window: u32,
1204    max_packet: u32,
1205) -> Vec<u8> {
1206    let type_str = channel_type.as_str().as_bytes();
1207    let mut buf = Vec::with_capacity(20 + type_str.len());
1208    buf.push(SSH_MSG_CHANNEL_OPEN);
1209    write_ssh_string(&mut buf, type_str);
1210    buf.extend_from_slice(&sender_channel.to_be_bytes());
1211    buf.extend_from_slice(&initial_window.to_be_bytes());
1212    buf.extend_from_slice(&max_packet.to_be_bytes());
1213    buf
1214}
1215
1216/// Parse a channel open request
1217pub(crate) fn parse_channel_open(payload: &[u8]) -> Result<(ChannelType, u32, u32, u32), SshError> {
1218    if payload.is_empty() || payload[0] != SSH_MSG_CHANNEL_OPEN {
1219        return Err(SshError::InvalidPacket);
1220    }
1221    let mut pos = 1;
1222    let type_name = read_ssh_string(payload, &mut pos)?;
1223    let channel_type = ChannelType::from_bytes(&type_name).ok_or(SshError::InvalidChannelType)?;
1224
1225    if pos + 12 > payload.len() {
1226        return Err(SshError::InvalidPacket);
1227    }
1228    let sender_channel = read_u32(payload, &mut pos)?;
1229    let initial_window = read_u32(payload, &mut pos)?;
1230    let max_packet = read_u32(payload, &mut pos)?;
1231
1232    Ok((channel_type, sender_channel, initial_window, max_packet))
1233}
1234
1235/// Encode channel open confirmation
1236pub(crate) fn encode_channel_open_confirmation(
1237    recipient_channel: u32,
1238    sender_channel: u32,
1239    initial_window: u32,
1240    max_packet: u32,
1241) -> Vec<u8> {
1242    let mut buf = Vec::with_capacity(17);
1243    buf.push(SSH_MSG_CHANNEL_OPEN_CONFIRMATION);
1244    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1245    buf.extend_from_slice(&sender_channel.to_be_bytes());
1246    buf.extend_from_slice(&initial_window.to_be_bytes());
1247    buf.extend_from_slice(&max_packet.to_be_bytes());
1248    buf
1249}
1250
1251/// Encode channel open failure
1252pub(crate) fn encode_channel_open_failure(
1253    recipient_channel: u32,
1254    reason_code: u32,
1255    description: &str,
1256) -> Vec<u8> {
1257    let desc_bytes = description.as_bytes();
1258    let mut buf = Vec::with_capacity(17 + desc_bytes.len());
1259    buf.push(SSH_MSG_CHANNEL_OPEN_FAILURE);
1260    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1261    buf.extend_from_slice(&reason_code.to_be_bytes());
1262    write_ssh_string(&mut buf, desc_bytes);
1263    // language tag (empty)
1264    write_ssh_string(&mut buf, b"");
1265    buf
1266}
1267
1268/// Encode channel data
1269pub(crate) fn encode_channel_data(recipient_channel: u32, data: &[u8]) -> Vec<u8> {
1270    let mut buf = Vec::with_capacity(9 + data.len());
1271    buf.push(SSH_MSG_CHANNEL_DATA);
1272    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1273    write_ssh_string(&mut buf, data);
1274    buf
1275}
1276
1277/// Parse channel data, returns (recipient_channel, data)
1278pub(crate) fn parse_channel_data(payload: &[u8]) -> Result<(u32, Vec<u8>), SshError> {
1279    if payload.is_empty() || payload[0] != SSH_MSG_CHANNEL_DATA {
1280        return Err(SshError::InvalidPacket);
1281    }
1282    let mut pos = 1;
1283    let recipient_channel = read_u32(payload, &mut pos)?;
1284    let data = read_ssh_string(payload, &mut pos)?;
1285    Ok((recipient_channel, data))
1286}
1287
1288/// Encode channel extended data (e.g., stderr)
1289pub(crate) fn encode_channel_extended_data(
1290    recipient_channel: u32,
1291    data_type: u32,
1292    data: &[u8],
1293) -> Vec<u8> {
1294    let mut buf = Vec::with_capacity(13 + data.len());
1295    buf.push(SSH_MSG_CHANNEL_EXTENDED_DATA);
1296    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1297    buf.extend_from_slice(&data_type.to_be_bytes());
1298    write_ssh_string(&mut buf, data);
1299    buf
1300}
1301
1302/// Encode channel window adjust
1303pub(crate) fn encode_window_adjust(recipient_channel: u32, bytes_to_add: u32) -> Vec<u8> {
1304    let mut buf = Vec::with_capacity(9);
1305    buf.push(SSH_MSG_CHANNEL_WINDOW_ADJUST);
1306    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1307    buf.extend_from_slice(&bytes_to_add.to_be_bytes());
1308    buf
1309}
1310
1311/// Parse channel window adjust, returns (recipient_channel, bytes_to_add)
1312pub(crate) fn parse_window_adjust(payload: &[u8]) -> Result<(u32, u32), SshError> {
1313    if payload.is_empty() || payload[0] != SSH_MSG_CHANNEL_WINDOW_ADJUST {
1314        return Err(SshError::InvalidPacket);
1315    }
1316    let mut pos = 1;
1317    let recipient_channel = read_u32(payload, &mut pos)?;
1318    let bytes_to_add = read_u32(payload, &mut pos)?;
1319    Ok((recipient_channel, bytes_to_add))
1320}
1321
1322/// Encode channel EOF
1323pub(crate) fn encode_channel_eof(recipient_channel: u32) -> Vec<u8> {
1324    let mut buf = Vec::with_capacity(5);
1325    buf.push(SSH_MSG_CHANNEL_EOF);
1326    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1327    buf
1328}
1329
1330/// Encode channel close
1331pub(crate) fn encode_channel_close(recipient_channel: u32) -> Vec<u8> {
1332    let mut buf = Vec::with_capacity(5);
1333    buf.push(SSH_MSG_CHANNEL_CLOSE);
1334    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1335    buf
1336}
1337
1338/// Encode a channel request
1339pub(crate) fn encode_channel_request(
1340    recipient_channel: u32,
1341    request_type: &str,
1342    want_reply: bool,
1343    data: &[u8],
1344) -> Vec<u8> {
1345    let type_bytes = request_type.as_bytes();
1346    let mut buf = Vec::with_capacity(10 + type_bytes.len() + data.len());
1347    buf.push(SSH_MSG_CHANNEL_REQUEST);
1348    buf.extend_from_slice(&recipient_channel.to_be_bytes());
1349    write_ssh_string(&mut buf, type_bytes);
1350    buf.push(if want_reply { 1 } else { 0 });
1351    buf.extend_from_slice(data);
1352    buf
1353}
1354
1355/// Parsed channel request
1356#[derive(Debug, Clone, PartialEq, Eq)]
1357pub struct ChannelRequest {
1358    /// Recipient channel
1359    pub recipient_channel: u32,
1360    /// Request type string
1361    pub request_type: Vec<u8>,
1362    /// Whether the sender wants a reply
1363    pub want_reply: bool,
1364    /// Type-specific data (remaining bytes)
1365    pub data: Vec<u8>,
1366}
1367
1368/// Parse a channel request
1369pub(crate) fn parse_channel_request(payload: &[u8]) -> Result<ChannelRequest, SshError> {
1370    if payload.is_empty() || payload[0] != SSH_MSG_CHANNEL_REQUEST {
1371        return Err(SshError::InvalidPacket);
1372    }
1373    let mut pos = 1;
1374    let recipient_channel = read_u32(payload, &mut pos)?;
1375    let request_type = read_ssh_string(payload, &mut pos)?;
1376    if pos >= payload.len() {
1377        return Err(SshError::InvalidPacket);
1378    }
1379    let want_reply = payload[pos] != 0;
1380    pos += 1;
1381    let data = if pos < payload.len() {
1382        payload[pos..].to_vec()
1383    } else {
1384        Vec::new()
1385    };
1386
1387    Ok(ChannelRequest {
1388        recipient_channel,
1389        request_type,
1390        want_reply,
1391        data,
1392    })
1393}
1394
1395// ============================================================================
1396// Section 4: Session Management (~200 lines)
1397// ============================================================================
1398
1399/// PTY terminal information
1400#[derive(Debug, Clone, PartialEq, Eq)]
1401pub struct PtyInfo {
1402    /// Terminal type (e.g., "xterm-256color")
1403    pub term_type: Vec<u8>,
1404    /// Terminal width in columns
1405    pub width_cols: u32,
1406    /// Terminal height in rows
1407    pub height_rows: u32,
1408    /// Terminal width in pixels
1409    pub width_pixels: u32,
1410    /// Terminal height in pixels
1411    pub height_pixels: u32,
1412    /// Terminal modes (encoded as per RFC 4254)
1413    pub terminal_modes: Vec<u8>,
1414}
1415
1416impl PtyInfo {
1417    /// Parse a pty-req request data blob
1418    pub(crate) fn parse(data: &[u8]) -> Result<Self, SshError> {
1419        let mut pos = 0;
1420        let term_type = read_ssh_string(data, &mut pos)?;
1421        let width_cols = read_u32(data, &mut pos)?;
1422        let height_rows = read_u32(data, &mut pos)?;
1423        let width_pixels = read_u32(data, &mut pos)?;
1424        let height_pixels = read_u32(data, &mut pos)?;
1425        let terminal_modes = read_ssh_string(data, &mut pos)?;
1426
1427        Ok(Self {
1428            term_type,
1429            width_cols,
1430            height_rows,
1431            width_pixels,
1432            height_pixels,
1433            terminal_modes,
1434        })
1435    }
1436
1437    /// Encode PTY info to wire format (for pty-req channel request data)
1438    pub(crate) fn encode(&self) -> Vec<u8> {
1439        let mut buf = Vec::with_capacity(32 + self.term_type.len() + self.terminal_modes.len());
1440        write_ssh_string(&mut buf, &self.term_type);
1441        buf.extend_from_slice(&self.width_cols.to_be_bytes());
1442        buf.extend_from_slice(&self.height_rows.to_be_bytes());
1443        buf.extend_from_slice(&self.width_pixels.to_be_bytes());
1444        buf.extend_from_slice(&self.height_pixels.to_be_bytes());
1445        write_ssh_string(&mut buf, &self.terminal_modes);
1446        buf
1447    }
1448}
1449
1450/// Exec request: single command execution
1451#[derive(Debug, Clone, PartialEq, Eq)]
1452pub struct ExecRequest {
1453    /// Command to execute
1454    pub command: Vec<u8>,
1455}
1456
1457impl ExecRequest {
1458    pub(crate) fn parse(data: &[u8]) -> Result<Self, SshError> {
1459        let mut pos = 0;
1460        let command = read_ssh_string(data, &mut pos)?;
1461        Ok(Self { command })
1462    }
1463}
1464
1465/// Environment variable request
1466#[derive(Debug, Clone, PartialEq, Eq)]
1467pub struct EnvRequest {
1468    /// Variable name
1469    pub name: Vec<u8>,
1470    /// Variable value
1471    pub value: Vec<u8>,
1472}
1473
1474impl EnvRequest {
1475    pub(crate) fn parse(data: &[u8]) -> Result<Self, SshError> {
1476        let mut pos = 0;
1477        let name = read_ssh_string(data, &mut pos)?;
1478        let value = read_ssh_string(data, &mut pos)?;
1479        Ok(Self { name, value })
1480    }
1481}
1482
1483/// Exit status payload (for "exit-status" channel request)
1484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1485pub struct ExitStatus {
1486    pub code: u32,
1487}
1488
1489impl ExitStatus {
1490    pub(crate) fn encode(&self) -> Vec<u8> {
1491        self.code.to_be_bytes().to_vec()
1492    }
1493
1494    pub(crate) fn parse(data: &[u8]) -> Result<Self, SshError> {
1495        if data.len() < 4 {
1496            return Err(SshError::InvalidPacket);
1497        }
1498        let code = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
1499        Ok(Self { code })
1500    }
1501}
1502
1503/// Exit signal payload (for "exit-signal" channel request)
1504#[derive(Debug, Clone, PartialEq, Eq)]
1505pub struct ExitSignal {
1506    /// Signal name (without "SIG" prefix, e.g., "TERM", "KILL")
1507    pub signal_name: Vec<u8>,
1508    /// Core dumped flag
1509    pub core_dumped: bool,
1510    /// Error message
1511    pub error_message: Vec<u8>,
1512    /// Language tag
1513    pub language_tag: Vec<u8>,
1514}
1515
1516impl ExitSignal {
1517    pub(crate) fn encode(&self) -> Vec<u8> {
1518        let mut buf = Vec::with_capacity(
1519            16 + self.signal_name.len() + self.error_message.len() + self.language_tag.len(),
1520        );
1521        write_ssh_string(&mut buf, &self.signal_name);
1522        buf.push(if self.core_dumped { 1 } else { 0 });
1523        write_ssh_string(&mut buf, &self.error_message);
1524        write_ssh_string(&mut buf, &self.language_tag);
1525        buf
1526    }
1527
1528    pub(crate) fn parse(data: &[u8]) -> Result<Self, SshError> {
1529        let mut pos = 0;
1530        let signal_name = read_ssh_string(data, &mut pos)?;
1531        if pos >= data.len() {
1532            return Err(SshError::InvalidPacket);
1533        }
1534        let core_dumped = data[pos] != 0;
1535        pos += 1;
1536        let error_message = read_ssh_string(data, &mut pos)?;
1537        let language_tag = read_ssh_string(data, &mut pos)?;
1538        Ok(Self {
1539            signal_name,
1540            core_dumped,
1541            error_message,
1542            language_tag,
1543        })
1544    }
1545}
1546
1547/// Shell session state
1548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1549pub enum ShellSessionState {
1550    /// Waiting for PTY or shell request
1551    Idle,
1552    /// PTY allocated, waiting for shell/exec
1553    PtyAllocated,
1554    /// Shell or command running
1555    Running,
1556    /// Session completed with exit status
1557    Exited,
1558}
1559
1560/// Represents a shell/exec session on a channel
1561#[derive(Debug, Clone, PartialEq, Eq)]
1562pub struct ShellSession {
1563    /// Channel ID this session is bound to
1564    pub channel_id: u32,
1565    /// Session state
1566    pub state: ShellSessionState,
1567    /// PTY info (if allocated)
1568    pub pty: Option<PtyInfo>,
1569    /// Command for exec requests (None for interactive shell)
1570    pub command: Option<Vec<u8>>,
1571    /// Environment variables
1572    pub environment: Vec<(Vec<u8>, Vec<u8>)>,
1573    /// Exit code (set when process exits)
1574    pub exit_code: Option<u32>,
1575    /// Exit signal (set if process killed by signal)
1576    pub exit_signal: Option<Vec<u8>>,
1577}
1578
1579impl ShellSession {
1580    /// Create a new idle shell session
1581    pub fn new(channel_id: u32) -> Self {
1582        Self {
1583            channel_id,
1584            state: ShellSessionState::Idle,
1585            pty: None,
1586            command: None,
1587            environment: Vec::new(),
1588            exit_code: None,
1589            exit_signal: None,
1590        }
1591    }
1592
1593    /// Allocate a PTY for this session
1594    pub(crate) fn allocate_pty(&mut self, pty_info: PtyInfo) -> Result<(), SshError> {
1595        if self.state != ShellSessionState::Idle {
1596            return Err(SshError::InvalidState);
1597        }
1598        self.pty = Some(pty_info);
1599        self.state = ShellSessionState::PtyAllocated;
1600        Ok(())
1601    }
1602
1603    /// Start a shell session
1604    pub(crate) fn start_shell(&mut self) -> Result<(), SshError> {
1605        match self.state {
1606            ShellSessionState::Idle | ShellSessionState::PtyAllocated => {
1607                self.state = ShellSessionState::Running;
1608                Ok(())
1609            }
1610            _ => Err(SshError::InvalidState),
1611        }
1612    }
1613
1614    /// Start an exec session
1615    pub fn start_exec(&mut self, command: Vec<u8>) -> Result<(), SshError> {
1616        match self.state {
1617            ShellSessionState::Idle | ShellSessionState::PtyAllocated => {
1618                self.command = Some(command);
1619                self.state = ShellSessionState::Running;
1620                Ok(())
1621            }
1622            _ => Err(SshError::InvalidState),
1623        }
1624    }
1625
1626    /// Add an environment variable
1627    pub(crate) fn set_env(&mut self, name: Vec<u8>, value: Vec<u8>) {
1628        self.environment.push((name, value));
1629    }
1630
1631    /// Mark session as exited
1632    pub(crate) fn mark_exited(&mut self, code: u32) {
1633        self.exit_code = Some(code);
1634        self.state = ShellSessionState::Exited;
1635    }
1636
1637    /// Mark session as killed by signal
1638    pub(crate) fn mark_signaled(&mut self, signal: Vec<u8>) {
1639        self.exit_signal = Some(signal);
1640        self.state = ShellSessionState::Exited;
1641    }
1642}
1643
1644// ============================================================================
1645// Section 5: SSH Server (~150 lines)
1646// ============================================================================
1647
1648/// Host key pair (Ed25519)
1649#[derive(Debug, Clone, PartialEq, Eq)]
1650pub struct HostKeyPair {
1651    /// Ed25519 public key (32 bytes)
1652    pub public_key: [u8; ED25519_PUB_LEN],
1653    /// Ed25519 private key (64 bytes: seed + public)
1654    pub private_key: [u8; 64],
1655}
1656
1657impl HostKeyPair {
1658    /// Create a host key pair from raw bytes
1659    pub fn new(public_key: [u8; 32], private_key: [u8; 64]) -> Self {
1660        Self {
1661            public_key,
1662            private_key,
1663        }
1664    }
1665
1666    /// Encode public key in SSH wire format ("ssh-ed25519" || key_data)
1667    pub(crate) fn encode_public_key(&self) -> Vec<u8> {
1668        let mut buf = Vec::with_capacity(51);
1669        write_ssh_string(&mut buf, b"ssh-ed25519");
1670        write_ssh_string(&mut buf, &self.public_key);
1671        buf
1672    }
1673}
1674
1675/// SSH session (one per connected client)
1676#[derive(Debug, Clone, PartialEq, Eq)]
1677pub struct SshSession {
1678    /// Unique session identifier
1679    pub session_id: u32,
1680    /// Current state
1681    pub state: SessionState,
1682    /// Authentication state
1683    pub auth: AuthState,
1684    /// Key exchange state
1685    pub kex: KexState,
1686    /// Transport keys (after key exchange)
1687    pub transport_keys: TransportKeys,
1688    /// Negotiated algorithms
1689    pub algorithms: NegotiatedAlgorithms,
1690    /// Channel table
1691    pub channels: ChannelTable,
1692    /// Shell sessions indexed by channel ID
1693    pub shell_sessions: BTreeMap<u32, ShellSession>,
1694    /// Session ID (exchange hash of first key exchange)
1695    pub session_hash: [u8; 32],
1696    /// Client version string
1697    pub client_version: Vec<u8>,
1698    /// Server version string
1699    pub server_version: Vec<u8>,
1700    /// Client KEXINIT payload (for exchange hash computation)
1701    pub client_kexinit: Vec<u8>,
1702    /// Server KEXINIT payload (for exchange hash computation)
1703    pub server_kexinit: Vec<u8>,
1704    /// Packet sequence number (send)
1705    pub send_seq: u32,
1706    /// Packet sequence number (recv)
1707    pub recv_seq: u32,
1708}
1709
1710impl SshSession {
1711    /// Create a new session in VersionExchange state
1712    pub fn new(session_id: u32) -> Self {
1713        Self {
1714            session_id,
1715            state: SessionState::VersionExchange,
1716            auth: AuthState::new(),
1717            kex: KexState::new(),
1718            transport_keys: TransportKeys::new(),
1719            algorithms: NegotiatedAlgorithms::new(),
1720            channels: ChannelTable::new(),
1721            shell_sessions: BTreeMap::new(),
1722            session_hash: [0u8; 32],
1723            client_version: Vec::new(),
1724            server_version: SSH_VERSION_STRING[..SSH_VERSION_STRING.len() - 2].to_vec(),
1725            client_kexinit: Vec::new(),
1726            server_kexinit: Vec::new(),
1727            send_seq: 0,
1728            recv_seq: 0,
1729        }
1730    }
1731
1732    /// Process version exchange
1733    pub(crate) fn process_version(&mut self, data: &[u8]) -> Result<SessionState, SshError> {
1734        if self.state != SessionState::VersionExchange {
1735            return Err(SshError::InvalidState);
1736        }
1737
1738        let version = VersionInfo::parse(data).ok_or(SshError::VersionMismatch)?;
1739        if &version.protocol_version != b"2.0" {
1740            return Err(SshError::VersionMismatch);
1741        }
1742
1743        self.client_version = data.to_vec();
1744        self.state = SessionState::KeyExchange;
1745        Ok(self.state)
1746    }
1747
1748    /// Transition to NewKeysExpected after KEXINIT exchange
1749    pub(crate) fn begin_key_exchange(&mut self) -> Result<(), SshError> {
1750        if self.state != SessionState::KeyExchange {
1751            return Err(SshError::InvalidState);
1752        }
1753        Ok(())
1754    }
1755
1756    /// Process NEWKEYS message, transition to Authentication
1757    pub(crate) fn process_newkeys(&mut self) -> Result<SessionState, SshError> {
1758        if self.state != SessionState::KeyExchange && self.state != SessionState::NewKeysExpected {
1759            return Err(SshError::InvalidState);
1760        }
1761        self.state = SessionState::Authentication;
1762        Ok(self.state)
1763    }
1764
1765    /// Process successful authentication, transition to Connected
1766    pub(crate) fn authenticate_success(&mut self) -> Result<SessionState, SshError> {
1767        if self.state != SessionState::Authentication {
1768            return Err(SshError::InvalidState);
1769        }
1770        self.auth.mark_success();
1771        self.state = SessionState::Connected;
1772        Ok(self.state)
1773    }
1774
1775    /// Disconnect the session
1776    pub(crate) fn disconnect(&mut self) {
1777        self.state = SessionState::Disconnected;
1778    }
1779
1780    /// Process a channel request (pty-req, shell, exec, env)
1781    pub(crate) fn handle_channel_request(
1782        &mut self,
1783        request: &ChannelRequest,
1784    ) -> Result<bool, SshError> {
1785        let channel = self
1786            .channels
1787            .get_mut(request.recipient_channel)
1788            .ok_or(SshError::ChannelNotFound)?;
1789
1790        match request.request_type.as_slice() {
1791            b"pty-req" => {
1792                let pty_info = PtyInfo::parse(&request.data)?;
1793                channel.pty_allocated = true;
1794                channel.pty_info = Some(pty_info.clone());
1795
1796                // Create/update shell session
1797                let session = self
1798                    .shell_sessions
1799                    .entry(request.recipient_channel)
1800                    .or_insert_with(|| ShellSession::new(request.recipient_channel));
1801                session.allocate_pty(pty_info)?;
1802                Ok(true)
1803            }
1804            b"shell" => {
1805                channel.session_started = true;
1806                let session = self
1807                    .shell_sessions
1808                    .entry(request.recipient_channel)
1809                    .or_insert_with(|| ShellSession::new(request.recipient_channel));
1810                session.start_shell()?;
1811                Ok(true)
1812            }
1813            b"exec" => {
1814                let exec_req = ExecRequest::parse(&request.data)?;
1815                channel.session_started = true;
1816                let session = self
1817                    .shell_sessions
1818                    .entry(request.recipient_channel)
1819                    .or_insert_with(|| ShellSession::new(request.recipient_channel));
1820                session.start_exec(exec_req.command)?;
1821                Ok(true)
1822            }
1823            b"env" => {
1824                let env_req = EnvRequest::parse(&request.data)?;
1825                let session = self
1826                    .shell_sessions
1827                    .entry(request.recipient_channel)
1828                    .or_insert_with(|| ShellSession::new(request.recipient_channel));
1829                session.set_env(env_req.name, env_req.value);
1830                Ok(true)
1831            }
1832            b"exit-status" => {
1833                let exit = ExitStatus::parse(&request.data)?;
1834                if let Some(session) = self.shell_sessions.get_mut(&request.recipient_channel) {
1835                    session.mark_exited(exit.code);
1836                }
1837                Ok(true)
1838            }
1839            b"exit-signal" => {
1840                let signal = ExitSignal::parse(&request.data)?;
1841                if let Some(session) = self.shell_sessions.get_mut(&request.recipient_channel) {
1842                    session.mark_signaled(signal.signal_name);
1843                }
1844                Ok(true)
1845            }
1846            _ => {
1847                // Unknown request type
1848                Ok(false)
1849            }
1850        }
1851    }
1852
1853    /// Increment send sequence counter
1854    pub(crate) fn next_send_seq(&mut self) -> u32 {
1855        let seq = self.send_seq;
1856        self.send_seq = self.send_seq.wrapping_add(1);
1857        seq
1858    }
1859
1860    /// Increment recv sequence counter
1861    pub(crate) fn next_recv_seq(&mut self) -> u32 {
1862        let seq = self.recv_seq;
1863        self.recv_seq = self.recv_seq.wrapping_add(1);
1864        seq
1865    }
1866}
1867
1868/// SSH server configuration and state
1869#[derive(Debug, Clone)]
1870pub struct SshServer {
1871    /// Server host key
1872    pub host_key: HostKeyPair,
1873    /// Listen port
1874    pub port: u16,
1875    /// Maximum concurrent sessions
1876    pub max_sessions: usize,
1877    /// Active sessions
1878    pub sessions: BTreeMap<u32, SshSession>,
1879    /// Next session ID
1880    pub next_session_id: u32,
1881    /// Server banner (optional, sent before auth)
1882    pub banner: Option<String>,
1883}
1884
1885impl SshServer {
1886    /// Create a new SSH server
1887    pub fn new(host_key: HostKeyPair, port: u16) -> Self {
1888        Self {
1889            host_key,
1890            port,
1891            max_sessions: MAX_SESSIONS,
1892            sessions: BTreeMap::new(),
1893            next_session_id: 0,
1894            banner: None,
1895        }
1896    }
1897
1898    /// Set the server banner message
1899    pub(crate) fn set_banner(&mut self, banner: String) {
1900        self.banner = Some(banner);
1901    }
1902
1903    /// Accept a new connection, creating a session
1904    pub(crate) fn accept_connection(&mut self) -> Result<u32, SshError> {
1905        if self.sessions.len() >= self.max_sessions {
1906            return Err(SshError::SessionLimitReached);
1907        }
1908        let id = self.next_session_id;
1909        self.next_session_id = self.next_session_id.wrapping_add(1);
1910        let session = SshSession::new(id);
1911        self.sessions.insert(id, session);
1912        Ok(id)
1913    }
1914
1915    /// Get a session by ID
1916    pub(crate) fn get_session(&self, session_id: u32) -> Option<&SshSession> {
1917        self.sessions.get(&session_id)
1918    }
1919
1920    /// Get a mutable session by ID
1921    pub(crate) fn get_session_mut(&mut self, session_id: u32) -> Option<&mut SshSession> {
1922        self.sessions.get_mut(&session_id)
1923    }
1924
1925    /// Remove a disconnected session
1926    pub(crate) fn remove_session(&mut self, session_id: u32) -> Option<SshSession> {
1927        self.sessions.remove(&session_id)
1928    }
1929
1930    /// Number of active sessions
1931    pub(crate) fn active_sessions(&self) -> usize {
1932        self.sessions.len()
1933    }
1934}
1935
1936// ============================================================================
1937// Wire encoding/decoding helpers
1938// ============================================================================
1939
1940/// Read a length-prefixed SSH string from a buffer, advancing pos
1941fn read_ssh_string(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, SshError> {
1942    if *pos + 4 > data.len() {
1943        return Err(SshError::InvalidPacket);
1944    }
1945    let len =
1946        u32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]) as usize;
1947    *pos += 4;
1948    if *pos + len > data.len() {
1949        return Err(SshError::InvalidPacket);
1950    }
1951    let result = data[*pos..*pos + len].to_vec();
1952    *pos += len;
1953    Ok(result)
1954}
1955
1956/// Write a length-prefixed SSH string to a buffer
1957fn write_ssh_string(buf: &mut Vec<u8>, data: &[u8]) {
1958    buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
1959    buf.extend_from_slice(data);
1960}
1961
1962/// Read a u32 from a buffer, advancing pos
1963fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, SshError> {
1964    if *pos + 4 > data.len() {
1965        return Err(SshError::InvalidPacket);
1966    }
1967    let val = u32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
1968    *pos += 4;
1969    Ok(val)
1970}
1971
1972// ============================================================================
1973// Tests
1974// ============================================================================
1975
1976#[cfg(test)]
1977mod tests {
1978    #[allow(unused_imports)]
1979    use alloc::vec;
1980
1981    use super::*;
1982
1983    // -----------------------------------------------------------------------
1984    // Version string parsing
1985    // -----------------------------------------------------------------------
1986
1987    #[test]
1988    fn test_version_parse_basic() {
1989        let input = b"SSH-2.0-OpenSSH_9.0\r\n";
1990        let v = VersionInfo::parse(input).unwrap();
1991        assert_eq!(&v.protocol_version, b"2.0");
1992        assert_eq!(&v.software_version, b"OpenSSH_9.0");
1993        assert!(v.comment.is_empty());
1994    }
1995
1996    #[test]
1997    fn test_version_parse_with_comment() {
1998        let input = b"SSH-2.0-VeridianOS_1.0 custom comment\r\n";
1999        let v = VersionInfo::parse(input).unwrap();
2000        assert_eq!(&v.software_version, b"VeridianOS_1.0");
2001        assert_eq!(&v.comment, b"custom comment");
2002    }
2003
2004    #[test]
2005    fn test_version_parse_no_crlf() {
2006        let input = b"SSH-2.0-TestClient\n";
2007        let v = VersionInfo::parse(input).unwrap();
2008        assert_eq!(&v.software_version, b"TestClient");
2009    }
2010
2011    #[test]
2012    fn test_version_parse_invalid_prefix() {
2013        assert!(VersionInfo::parse(b"TLS-2.0-Client\r\n").is_none());
2014    }
2015
2016    #[test]
2017    fn test_version_parse_wrong_protocol() {
2018        assert!(VersionInfo::parse(b"SSH-1.0-OldClient\r\n").is_none());
2019    }
2020
2021    #[test]
2022    fn test_version_encode_roundtrip() {
2023        let input = b"SSH-2.0-TestServer\r\n";
2024        let v = VersionInfo::parse(input).unwrap();
2025        let encoded = v.encode();
2026        assert_eq!(&encoded, input);
2027    }
2028
2029    // -----------------------------------------------------------------------
2030    // Binary packet encode/decode
2031    // -----------------------------------------------------------------------
2032
2033    #[test]
2034    fn test_packet_encode_decode_roundtrip() {
2035        let payload = vec![SSH_MSG_IGNORE, 0x01, 0x02, 0x03];
2036        let packet = SshPacket::new(payload.clone());
2037        let encoded = packet.encode();
2038
2039        let (decoded, consumed) = SshPacket::decode(&encoded).unwrap();
2040        assert_eq!(consumed, encoded.len());
2041        assert_eq!(decoded.payload, payload);
2042    }
2043
2044    #[test]
2045    fn test_packet_minimum_padding() {
2046        let payload = vec![SSH_MSG_IGNORE];
2047        let packet = SshPacket::new(payload);
2048        assert!(packet.padding.len() >= MIN_PADDING);
2049    }
2050
2051    #[test]
2052    fn test_packet_alignment() {
2053        let payload = vec![0u8; 17];
2054        let packet = SshPacket::new(payload);
2055        let encoded = packet.encode();
2056        // Total encoded length (excluding MAC) should be multiple of block size
2057        assert_eq!(encoded.len() % BLOCK_SIZE_CLEAR, 0);
2058    }
2059
2060    #[test]
2061    fn test_packet_decode_too_short() {
2062        let data = [0u8; 3];
2063        assert_eq!(SshPacket::decode(&data), Err(SshError::InvalidPacket));
2064    }
2065
2066    #[test]
2067    fn test_packet_decode_truncated() {
2068        // packet_length=100 but only 10 bytes available
2069        let mut data = vec![0, 0, 0, 100, 4];
2070        data.extend_from_slice(&[0u8; 5]);
2071        assert_eq!(SshPacket::decode(&data), Err(SshError::InvalidPacket));
2072    }
2073
2074    #[test]
2075    fn test_packet_message_type() {
2076        let packet = SshPacket::new(vec![SSH_MSG_KEXINIT, 0, 0]);
2077        assert_eq!(packet.message_type(), Some(SSH_MSG_KEXINIT));
2078    }
2079
2080    #[test]
2081    fn test_packet_empty_payload_message_type() {
2082        let packet = SshPacket {
2083            payload: Vec::new(),
2084            padding: vec![0u8; 4],
2085        };
2086        assert_eq!(packet.message_type(), None);
2087    }
2088
2089    // -----------------------------------------------------------------------
2090    // KEXINIT message
2091    // -----------------------------------------------------------------------
2092
2093    #[test]
2094    fn test_kexinit_encode_decode_roundtrip() {
2095        let cookie = [0xAA; 16];
2096        let msg = KexInitMessage::new_server(cookie);
2097        let encoded = msg.encode();
2098
2099        assert_eq!(encoded[0], SSH_MSG_KEXINIT);
2100        assert_eq!(&encoded[1..17], &cookie);
2101
2102        let decoded = KexInitMessage::decode(&encoded).unwrap();
2103        assert_eq!(decoded.cookie, cookie);
2104        assert_eq!(decoded.kex_algorithms, msg.kex_algorithms);
2105        assert_eq!(
2106            decoded.server_host_key_algorithms,
2107            msg.server_host_key_algorithms
2108        );
2109        assert!(!decoded.first_kex_packet_follows);
2110    }
2111
2112    #[test]
2113    fn test_kexinit_decode_wrong_type() {
2114        let data = vec![SSH_MSG_NEWKEYS; 20];
2115        assert_eq!(KexInitMessage::decode(&data), Err(SshError::InvalidPacket));
2116    }
2117
2118    #[test]
2119    fn test_kexinit_decode_too_short() {
2120        let data = vec![SSH_MSG_KEXINIT; 10];
2121        assert_eq!(KexInitMessage::decode(&data), Err(SshError::InvalidPacket));
2122    }
2123
2124    // -----------------------------------------------------------------------
2125    // Channel open/confirm/close lifecycle
2126    // -----------------------------------------------------------------------
2127
2128    #[test]
2129    fn test_channel_lifecycle() {
2130        let mut table = ChannelTable::new();
2131
2132        // Open
2133        let id = table.open(ChannelType::Session).unwrap();
2134        assert_eq!(id, 0);
2135        assert_eq!(table.count(), 1);
2136
2137        // Confirm
2138        {
2139            let ch = table.get_mut(id).unwrap();
2140            assert_eq!(ch.state, ChannelState::Opening);
2141            ch.confirm(42, DEFAULT_WINDOW_SIZE, MAX_CHANNEL_DATA_SIZE);
2142            assert_eq!(ch.state, ChannelState::Open);
2143            assert_eq!(ch.remote_id, 42);
2144        }
2145
2146        // EOF
2147        {
2148            let ch = table.get_mut(id).unwrap();
2149            ch.mark_eof_received();
2150            assert_eq!(ch.state, ChannelState::EofReceived);
2151        }
2152
2153        // Close
2154        {
2155            let ch = table.get_mut(id).unwrap();
2156            ch.close();
2157            assert_eq!(ch.state, ChannelState::Closed);
2158        }
2159
2160        // Remove
2161        let removed = table.remove(id);
2162        assert!(removed.is_some());
2163        assert_eq!(table.count(), 0);
2164    }
2165
2166    #[test]
2167    fn test_channel_open_multiple() {
2168        let mut table = ChannelTable::new();
2169        let id1 = table.open(ChannelType::Session).unwrap();
2170        let id2 = table.open(ChannelType::Session).unwrap();
2171        assert_ne!(id1, id2);
2172        assert_eq!(table.count(), 2);
2173    }
2174
2175    // -----------------------------------------------------------------------
2176    // Window adjust tracking
2177    // -----------------------------------------------------------------------
2178
2179    #[test]
2180    fn test_window_adjust() {
2181        let mut ch = Channel::new(0, ChannelType::Session);
2182        ch.confirm(1, 1024, 512);
2183
2184        assert!(ch.consume_send_window(512));
2185        assert_eq!(ch.remote_window, 512);
2186        assert!(ch.consume_send_window(512));
2187        assert_eq!(ch.remote_window, 0);
2188        assert!(!ch.consume_send_window(1));
2189
2190        ch.adjust_remote_window(2048);
2191        assert_eq!(ch.remote_window, 2048);
2192    }
2193
2194    #[test]
2195    fn test_window_adjust_saturation() {
2196        let mut ch = Channel::new(0, ChannelType::Session);
2197        ch.remote_window = u32::MAX - 10;
2198        ch.adjust_remote_window(100);
2199        assert_eq!(ch.remote_window, u32::MAX);
2200    }
2201
2202    #[test]
2203    fn test_local_window_consume() {
2204        let mut ch = Channel::new(0, ChannelType::Session);
2205        let initial = ch.local_window;
2206        assert!(ch.consume_recv_window(1024));
2207        assert_eq!(ch.local_window, initial - 1024);
2208
2209        ch.adjust_local_window(2048);
2210        assert_eq!(ch.local_window, initial - 1024 + 2048);
2211    }
2212
2213    // -----------------------------------------------------------------------
2214    // Window adjust message encode/parse
2215    // -----------------------------------------------------------------------
2216
2217    #[test]
2218    fn test_window_adjust_encode_parse() {
2219        let encoded = encode_window_adjust(7, 65536);
2220        let (ch, bytes) = parse_window_adjust(&encoded).unwrap();
2221        assert_eq!(ch, 7);
2222        assert_eq!(bytes, 65536);
2223    }
2224
2225    // -----------------------------------------------------------------------
2226    // Auth request parsing
2227    // -----------------------------------------------------------------------
2228
2229    #[test]
2230    fn test_parse_password_auth() {
2231        let mut payload = vec![SSH_MSG_USERAUTH_REQUEST];
2232        // username: "alice"
2233        payload.extend_from_slice(&5u32.to_be_bytes());
2234        payload.extend_from_slice(b"alice");
2235        // service: "ssh-connection"
2236        payload.extend_from_slice(&14u32.to_be_bytes());
2237        payload.extend_from_slice(b"ssh-connection");
2238        // method: "password"
2239        payload.extend_from_slice(&8u32.to_be_bytes());
2240        payload.extend_from_slice(b"password");
2241        // change password: false
2242        payload.push(0);
2243        // password: "secret"
2244        payload.extend_from_slice(&6u32.to_be_bytes());
2245        payload.extend_from_slice(b"secret");
2246
2247        let req = parse_userauth_request(&payload).unwrap();
2248        assert_eq!(&req.username, b"alice");
2249        assert_eq!(&req.service_name, b"ssh-connection");
2250        assert_eq!(req.method, AuthMethod::Password);
2251        match &req.method_data {
2252            AuthMethodData::Password { password } => assert_eq!(password, b"secret"),
2253            _ => panic!("Expected password auth data"),
2254        }
2255    }
2256
2257    #[test]
2258    fn test_parse_pubkey_auth_query() {
2259        let mut payload = vec![SSH_MSG_USERAUTH_REQUEST];
2260        // username
2261        payload.extend_from_slice(&3u32.to_be_bytes());
2262        payload.extend_from_slice(b"bob");
2263        // service
2264        payload.extend_from_slice(&14u32.to_be_bytes());
2265        payload.extend_from_slice(b"ssh-connection");
2266        // method
2267        payload.extend_from_slice(&9u32.to_be_bytes());
2268        payload.extend_from_slice(b"publickey");
2269        // has_signature: false
2270        payload.push(0);
2271        // algorithm
2272        payload.extend_from_slice(&11u32.to_be_bytes());
2273        payload.extend_from_slice(b"ssh-ed25519");
2274        // public key blob (dummy 32 bytes)
2275        let fake_key = [0x42u8; 32];
2276        payload.extend_from_slice(&32u32.to_be_bytes());
2277        payload.extend_from_slice(&fake_key);
2278
2279        let req = parse_userauth_request(&payload).unwrap();
2280        assert_eq!(req.method, AuthMethod::PublicKey);
2281        match &req.method_data {
2282            AuthMethodData::PublicKey {
2283                has_signature,
2284                algorithm,
2285                public_key,
2286                signature,
2287            } => {
2288                assert!(!(*has_signature));
2289                assert_eq!(algorithm, b"ssh-ed25519");
2290                assert_eq!(public_key.len(), 32);
2291                assert!(signature.is_empty());
2292            }
2293            _ => panic!("Expected public key auth data"),
2294        }
2295    }
2296
2297    // -----------------------------------------------------------------------
2298    // Session state transitions
2299    // -----------------------------------------------------------------------
2300
2301    #[test]
2302    fn test_session_state_transitions() {
2303        let mut session = SshSession::new(0);
2304        assert_eq!(session.state, SessionState::VersionExchange);
2305
2306        // Version exchange
2307        let new_state = session.process_version(b"SSH-2.0-TestClient\r\n").unwrap();
2308        assert_eq!(new_state, SessionState::KeyExchange);
2309
2310        // NEWKEYS
2311        let new_state = session.process_newkeys().unwrap();
2312        assert_eq!(new_state, SessionState::Authentication);
2313
2314        // Auth success
2315        let new_state = session.authenticate_success().unwrap();
2316        assert_eq!(new_state, SessionState::Connected);
2317
2318        // Disconnect
2319        session.disconnect();
2320        assert_eq!(session.state, SessionState::Disconnected);
2321    }
2322
2323    #[test]
2324    fn test_session_invalid_state_transition() {
2325        let mut session = SshSession::new(0);
2326        // Cannot go directly to newkeys from version exchange
2327        assert_eq!(session.process_newkeys(), Err(SshError::InvalidState));
2328    }
2329
2330    #[test]
2331    fn test_session_version_mismatch() {
2332        let mut session = SshSession::new(0);
2333        assert_eq!(
2334            session.process_version(b"SSH-1.0-OldClient\r\n"),
2335            Err(SshError::VersionMismatch)
2336        );
2337    }
2338
2339    // -----------------------------------------------------------------------
2340    // Channel data framing
2341    // -----------------------------------------------------------------------
2342
2343    #[test]
2344    fn test_channel_data_encode_parse() {
2345        let data = b"Hello, SSH world!";
2346        let encoded = encode_channel_data(5, data);
2347        let (ch, parsed_data) = parse_channel_data(&encoded).unwrap();
2348        assert_eq!(ch, 5);
2349        assert_eq!(&parsed_data, data);
2350    }
2351
2352    #[test]
2353    fn test_channel_data_empty() {
2354        let encoded = encode_channel_data(0, b"");
2355        let (ch, parsed_data) = parse_channel_data(&encoded).unwrap();
2356        assert_eq!(ch, 0);
2357        assert!(parsed_data.is_empty());
2358    }
2359
2360    // -----------------------------------------------------------------------
2361    // Disconnect message
2362    // -----------------------------------------------------------------------
2363
2364    #[test]
2365    fn test_disconnect_encode() {
2366        let msg = encode_disconnect(SSH_DISCONNECT_BY_APPLICATION, "goodbye");
2367        assert_eq!(msg[0], SSH_MSG_DISCONNECT);
2368        let reason = u32::from_be_bytes([msg[1], msg[2], msg[3], msg[4]]);
2369        assert_eq!(reason, SSH_DISCONNECT_BY_APPLICATION);
2370    }
2371
2372    // -----------------------------------------------------------------------
2373    // PTY request parsing
2374    // -----------------------------------------------------------------------
2375
2376    #[test]
2377    fn test_pty_request_parse() {
2378        let mut data = Vec::new();
2379        // term type: "xterm-256color"
2380        write_ssh_string(&mut data, b"xterm-256color");
2381        // width cols
2382        data.extend_from_slice(&80u32.to_be_bytes());
2383        // height rows
2384        data.extend_from_slice(&24u32.to_be_bytes());
2385        // width pixels
2386        data.extend_from_slice(&640u32.to_be_bytes());
2387        // height pixels
2388        data.extend_from_slice(&480u32.to_be_bytes());
2389        // terminal modes (empty)
2390        write_ssh_string(&mut data, b"");
2391
2392        let pty = PtyInfo::parse(&data).unwrap();
2393        assert_eq!(&pty.term_type, b"xterm-256color");
2394        assert_eq!(pty.width_cols, 80);
2395        assert_eq!(pty.height_rows, 24);
2396        assert_eq!(pty.width_pixels, 640);
2397        assert_eq!(pty.height_pixels, 480);
2398    }
2399
2400    #[test]
2401    fn test_pty_encode_roundtrip() {
2402        let pty = PtyInfo {
2403            term_type: b"vt100".to_vec(),
2404            width_cols: 132,
2405            height_rows: 43,
2406            width_pixels: 0,
2407            height_pixels: 0,
2408            terminal_modes: vec![0],
2409        };
2410        let encoded = pty.encode();
2411        let decoded = PtyInfo::parse(&encoded).unwrap();
2412        assert_eq!(decoded.term_type, pty.term_type);
2413        assert_eq!(decoded.width_cols, pty.width_cols);
2414        assert_eq!(decoded.height_rows, pty.height_rows);
2415    }
2416
2417    // -----------------------------------------------------------------------
2418    // Auth state tracking
2419    // -----------------------------------------------------------------------
2420
2421    #[test]
2422    fn test_auth_state_attempts() {
2423        let mut auth = AuthState::new();
2424        assert!(!auth.exhausted());
2425
2426        for _ in 0..MAX_AUTH_ATTEMPTS - 1 {
2427            assert!(auth.record_failure());
2428        }
2429        assert!(!auth.record_failure());
2430        assert!(auth.exhausted());
2431    }
2432
2433    #[test]
2434    fn test_auth_failure_message() {
2435        let methods = [AuthMethod::Password, AuthMethod::PublicKey];
2436        let msg = encode_userauth_failure(&methods, false);
2437        assert_eq!(msg[0], SSH_MSG_USERAUTH_FAILURE);
2438        // Last byte is partial_success = 0
2439        assert_eq!(*msg.last().unwrap(), 0);
2440    }
2441
2442    // -----------------------------------------------------------------------
2443    // Service request
2444    // -----------------------------------------------------------------------
2445
2446    #[test]
2447    fn test_service_request_parse() {
2448        let mut payload = vec![SSH_MSG_SERVICE_REQUEST];
2449        payload.extend_from_slice(&12u32.to_be_bytes());
2450        payload.extend_from_slice(b"ssh-userauth");
2451        let name = parse_service_request(&payload).unwrap();
2452        assert_eq!(&name, b"ssh-userauth");
2453    }
2454
2455    // -----------------------------------------------------------------------
2456    // Channel request parsing
2457    // -----------------------------------------------------------------------
2458
2459    #[test]
2460    fn test_channel_request_parse() {
2461        let encoded = encode_channel_request(3, "shell", true, b"");
2462        let req = parse_channel_request(&encoded).unwrap();
2463        assert_eq!(req.recipient_channel, 3);
2464        assert_eq!(&req.request_type, b"shell");
2465        assert!(req.want_reply);
2466        assert!(req.data.is_empty());
2467    }
2468
2469    #[test]
2470    fn test_exec_request_parse() {
2471        let mut exec_data = Vec::new();
2472        write_ssh_string(&mut exec_data, b"ls -la /tmp");
2473        let encoded = encode_channel_request(1, "exec", true, &exec_data);
2474        let req = parse_channel_request(&encoded).unwrap();
2475        assert_eq!(&req.request_type, b"exec");
2476
2477        let exec = ExecRequest::parse(&req.data).unwrap();
2478        assert_eq!(&exec.command, b"ls -la /tmp");
2479    }
2480
2481    // -----------------------------------------------------------------------
2482    // Channel open encode/parse
2483    // -----------------------------------------------------------------------
2484
2485    #[test]
2486    fn test_channel_open_encode_parse() {
2487        let encoded = encode_channel_open(
2488            ChannelType::Session,
2489            0,
2490            DEFAULT_WINDOW_SIZE,
2491            MAX_CHANNEL_DATA_SIZE,
2492        );
2493        let (ct, sender, window, max_pkt) = parse_channel_open(&encoded).unwrap();
2494        assert_eq!(ct, ChannelType::Session);
2495        assert_eq!(sender, 0);
2496        assert_eq!(window, DEFAULT_WINDOW_SIZE);
2497        assert_eq!(max_pkt, MAX_CHANNEL_DATA_SIZE);
2498    }
2499
2500    // -----------------------------------------------------------------------
2501    // Shell session management
2502    // -----------------------------------------------------------------------
2503
2504    #[test]
2505    fn test_shell_session_lifecycle() {
2506        let mut session = ShellSession::new(0);
2507        assert_eq!(session.state, ShellSessionState::Idle);
2508
2509        let pty = PtyInfo {
2510            term_type: b"xterm".to_vec(),
2511            width_cols: 80,
2512            height_rows: 24,
2513            width_pixels: 0,
2514            height_pixels: 0,
2515            terminal_modes: Vec::new(),
2516        };
2517        session.allocate_pty(pty).unwrap();
2518        assert_eq!(session.state, ShellSessionState::PtyAllocated);
2519
2520        session.start_shell().unwrap();
2521        assert_eq!(session.state, ShellSessionState::Running);
2522
2523        session.mark_exited(0);
2524        assert_eq!(session.state, ShellSessionState::Exited);
2525        assert_eq!(session.exit_code, Some(0));
2526    }
2527
2528    #[test]
2529    fn test_shell_session_exec() {
2530        let mut session = ShellSession::new(1);
2531        session.start_exec(b"uname -a".to_vec()).unwrap();
2532        assert_eq!(session.state, ShellSessionState::Running);
2533        assert_eq!(session.command.as_deref(), Some(b"uname -a".as_slice()));
2534    }
2535
2536    #[test]
2537    fn test_shell_session_invalid_transition() {
2538        let mut session = ShellSession::new(0);
2539        session.start_shell().unwrap();
2540        // Cannot allocate PTY after shell started
2541        let pty = PtyInfo {
2542            term_type: b"vt100".to_vec(),
2543            width_cols: 80,
2544            height_rows: 24,
2545            width_pixels: 0,
2546            height_pixels: 0,
2547            terminal_modes: Vec::new(),
2548        };
2549        assert_eq!(session.allocate_pty(pty), Err(SshError::InvalidState));
2550    }
2551
2552    // -----------------------------------------------------------------------
2553    // SSH server
2554    // -----------------------------------------------------------------------
2555
2556    #[test]
2557    fn test_server_accept_connection() {
2558        let host_key = HostKeyPair::new([0x11; 32], [0x22; 64]);
2559        let mut server = SshServer::new(host_key, 22);
2560        let id = server.accept_connection().unwrap();
2561        assert_eq!(id, 0);
2562        assert_eq!(server.active_sessions(), 1);
2563
2564        let session = server.get_session(id).unwrap();
2565        assert_eq!(session.state, SessionState::VersionExchange);
2566    }
2567
2568    #[test]
2569    fn test_server_session_limit() {
2570        let host_key = HostKeyPair::new([0x11; 32], [0x22; 64]);
2571        let mut server = SshServer::new(host_key, 22);
2572        server.max_sessions = 2;
2573
2574        server.accept_connection().unwrap();
2575        server.accept_connection().unwrap();
2576        assert_eq!(
2577            server.accept_connection(),
2578            Err(SshError::SessionLimitReached)
2579        );
2580    }
2581
2582    // -----------------------------------------------------------------------
2583    // Exit status/signal
2584    // -----------------------------------------------------------------------
2585
2586    #[test]
2587    fn test_exit_status_encode_parse() {
2588        let status = ExitStatus { code: 42 };
2589        let encoded = status.encode();
2590        let parsed = ExitStatus::parse(&encoded).unwrap();
2591        assert_eq!(parsed.code, 42);
2592    }
2593
2594    #[test]
2595    fn test_exit_signal_encode_parse() {
2596        let sig = ExitSignal {
2597            signal_name: b"TERM".to_vec(),
2598            core_dumped: false,
2599            error_message: b"terminated".to_vec(),
2600            language_tag: b"".to_vec(),
2601        };
2602        let encoded = sig.encode();
2603        let parsed = ExitSignal::parse(&encoded).unwrap();
2604        assert_eq!(&parsed.signal_name, b"TERM");
2605        assert!(!parsed.core_dumped);
2606        assert_eq!(&parsed.error_message, b"terminated");
2607    }
2608
2609    // -----------------------------------------------------------------------
2610    // Env request
2611    // -----------------------------------------------------------------------
2612
2613    #[test]
2614    fn test_env_request_parse() {
2615        let mut data = Vec::new();
2616        write_ssh_string(&mut data, b"LANG");
2617        write_ssh_string(&mut data, b"en_US.UTF-8");
2618        let env = EnvRequest::parse(&data).unwrap();
2619        assert_eq!(&env.name, b"LANG");
2620        assert_eq!(&env.value, b"en_US.UTF-8");
2621    }
2622
2623    // -----------------------------------------------------------------------
2624    // Host key encoding
2625    // -----------------------------------------------------------------------
2626
2627    #[test]
2628    fn test_host_key_encode() {
2629        let hk = HostKeyPair::new([0xAA; 32], [0xBB; 64]);
2630        let encoded = hk.encode_public_key();
2631        // Should contain "ssh-ed25519" prefix
2632        let mut pos = 0;
2633        let algo = read_ssh_string(&encoded, &mut pos).unwrap();
2634        assert_eq!(&algo, b"ssh-ed25519");
2635        let key = read_ssh_string(&encoded, &mut pos).unwrap();
2636        assert_eq!(key.len(), 32);
2637    }
2638
2639    // -----------------------------------------------------------------------
2640    // Sequence counters
2641    // -----------------------------------------------------------------------
2642
2643    #[test]
2644    fn test_sequence_counters() {
2645        let mut session = SshSession::new(0);
2646        assert_eq!(session.next_send_seq(), 0);
2647        assert_eq!(session.next_send_seq(), 1);
2648        assert_eq!(session.next_recv_seq(), 0);
2649        assert_eq!(session.next_recv_seq(), 1);
2650    }
2651
2652    // -----------------------------------------------------------------------
2653    // Banner
2654    // -----------------------------------------------------------------------
2655
2656    #[test]
2657    fn test_banner_encode() {
2658        let msg = encode_banner("Welcome to VeridianOS SSH\r\n");
2659        assert_eq!(msg[0], SSH_MSG_USERAUTH_BANNER);
2660    }
2661
2662    // -----------------------------------------------------------------------
2663    // Algorithm negotiation
2664    // -----------------------------------------------------------------------
2665
2666    #[test]
2667    fn test_algorithm_from_str() {
2668        assert_eq!(
2669            AlgorithmId::parse_name("curve25519-sha256"),
2670            Some(AlgorithmId::Curve25519Sha256)
2671        );
2672        assert_eq!(
2673            AlgorithmId::parse_name("ssh-ed25519"),
2674            Some(AlgorithmId::SshEd25519)
2675        );
2676        assert_eq!(AlgorithmId::parse_name("unknown-algo"), None);
2677    }
2678
2679    #[test]
2680    fn test_algorithm_roundtrip() {
2681        let algos = [
2682            AlgorithmId::Curve25519Sha256,
2683            AlgorithmId::SshEd25519,
2684            AlgorithmId::Chacha20Poly1305,
2685            AlgorithmId::Aes256Ctr,
2686            AlgorithmId::HmacSha256,
2687            AlgorithmId::None,
2688        ];
2689        for algo in &algos {
2690            let name = algo.as_str();
2691            let parsed = AlgorithmId::parse_name(name).unwrap();
2692            assert_eq!(*algo, parsed);
2693        }
2694    }
2695}