metalssh 0.0.1

Experimental SSH implementation
use crate::constants::msg::SSH_MSG_KEXINIT;
use crate::types::Error;
use crate::types::Result;
use crate::wire::SshDecode;
use crate::wire::SshEncode;

/// [`SSH_MSG_KEXINIT`]
pub struct Kexinit<'b> {
    /// Backing byte storage for this message.
    buffer: &'b [u8],
    pub message_code: u8,
    pub cookie: &'b [u8; 16],
    pub kex_algorithms: &'b [u8],
    pub server_host_key_algorithms: &'b [u8],
    pub encryption_algorithms_client_to_server: &'b [u8],
    pub encryption_algorithms_server_to_client: &'b [u8],
    pub mac_algorithms_client_to_server: &'b [u8],
    pub mac_algorithms_server_to_client: &'b [u8],
    pub compression_algorithms_client_to_server: &'b [u8],
    pub compression_algorithms_server_to_client: &'b [u8],
    pub languages_client_to_server: &'b [u8],
    pub languages_server_to_client: &'b [u8],
    pub first_kex_packet_follows: bool,
    pub reserved: u32,
}

impl<'b> Kexinit<'b> {
    // TODO: Check capacity to write into
    /// Writes this message into a buffer in place.
    ///
    /// # Panics
    ///
    /// - TODO: Figure out why it could panic
    #[allow(clippy::too_many_arguments)]
    pub fn write(
        mut buffer: &'b mut [u8],
        cookie: &[u8; 16],
        kex_algorithms: &[u8],
        server_host_key_algorithms: &[u8],
        encryption_algorithms_client_to_server: &[u8],
        encryption_algorithms_server_to_client: &[u8],
        mac_algorithms_client_to_server: &[u8],
        mac_algorithms_server_to_client: &[u8],
        compression_algorithms_client_to_server: &[u8],
        compression_algorithms_server_to_client: &[u8],
        languages_client_to_server: &[u8],
        languages_server_to_client: &[u8],
        first_kex_packet_follows: bool,
        reserved: u32,
    ) -> Result<(Self, usize)> {
        let mut offset = 0;

        buffer.write_byte(SSH_MSG_KEXINIT, &mut offset)?;

        let offset_cookie = offset;
        buffer.write_bytes_exact(cookie, &mut offset)?;

        let offset_kex_algorithms = offset;
        buffer.write_byte_string(kex_algorithms, &mut offset)?;

        let offset_server_host_key_algorithms = offset;
        buffer.write_byte_string(server_host_key_algorithms, &mut offset)?;

        let offset_encryption_algorithms_client_to_server = offset;
        buffer.write_byte_string(encryption_algorithms_client_to_server, &mut offset)?;

        let offset_encryption_algorithms_server_to_client = offset;
        buffer.write_byte_string(encryption_algorithms_server_to_client, &mut offset)?;

        let offset_mac_algorithms_client_to_server = offset;
        buffer.write_byte_string(mac_algorithms_client_to_server, &mut offset)?;

        let offset_mac_algorithms_server_to_client = offset;
        buffer.write_byte_string(mac_algorithms_server_to_client, &mut offset)?;

        let offset_compression_algorithms_client_to_server = offset;
        buffer.write_byte_string(compression_algorithms_client_to_server, &mut offset)?;

        let offset_compression_algorithms_server_to_client = offset;
        buffer.write_byte_string(compression_algorithms_server_to_client, &mut offset)?;

        let offset_languages_client_to_server = offset;
        buffer.write_byte_string(languages_client_to_server, &mut offset)?;

        let offset_languages_server_to_client = offset;
        buffer.write_byte_string(languages_server_to_client, &mut offset)?;

        let offset_first_kex_packet_follows = offset;
        buffer.write_boolean(first_kex_packet_follows, &mut offset)?;

        buffer.write_uint32(reserved, &mut offset)?;

        let buffer = &*buffer;

        #[rustfmt::skip]
        let message = Self {
            buffer,
            message_code: SSH_MSG_KEXINIT,
            cookie: buffer[offset_cookie..offset_kex_algorithms]
                .as_array()
                .unwrap(),
            kex_algorithms: &buffer[
                4 + offset_kex_algorithms
                ..offset_server_host_key_algorithms
            ],
            server_host_key_algorithms: &buffer[
                4 + offset_server_host_key_algorithms
                ..offset_encryption_algorithms_client_to_server
            ],
            encryption_algorithms_client_to_server: &buffer[
                4 + offset_encryption_algorithms_client_to_server
                ..offset_encryption_algorithms_server_to_client
            ],
            encryption_algorithms_server_to_client: &buffer[
                4 + offset_encryption_algorithms_server_to_client
                ..offset_mac_algorithms_client_to_server
            ],
            mac_algorithms_client_to_server: &buffer[
                4 + offset_mac_algorithms_client_to_server
                ..offset_mac_algorithms_server_to_client
            ],
            mac_algorithms_server_to_client: &buffer[
                4 + offset_mac_algorithms_server_to_client
                ..offset_compression_algorithms_client_to_server
            ],
            compression_algorithms_client_to_server: &buffer[
                4 + offset_compression_algorithms_client_to_server
                ..offset_compression_algorithms_server_to_client
            ],
            compression_algorithms_server_to_client: &buffer[
                4 + offset_compression_algorithms_server_to_client
                ..offset_languages_client_to_server
            ],
            languages_client_to_server: &buffer[
                4 + offset_languages_client_to_server
                ..offset_languages_server_to_client
            ],
            languages_server_to_client: &buffer[
                4 + offset_languages_server_to_client
                ..offset_first_kex_packet_follows
            ],
            first_kex_packet_follows,
            reserved,
        };

        Ok((message, offset))
    }

    /// Interprets an existing byte buffer as this message type.
    ///
    /// # Panics
    ///
    /// - TODO: Figure out why it could panic
    pub fn from_bytes(buffer: &'b [u8]) -> Result<Self> {
        let mut offset = 0;

        let message_code = buffer.read_byte(&mut offset)?;
        if message_code != SSH_MSG_KEXINIT {
            return Err(Error::UnexpectedMessage(message_code));
        }

        let cookie = buffer
            .read_bytes_exact(&mut offset, 16)?
            .as_array()
            .unwrap();
        let kex_algorithms = buffer.read_byte_string(&mut offset)?;
        let server_host_key_algorithms = buffer.read_byte_string(&mut offset)?;
        let encryption_algorithms_client_to_server = buffer.read_byte_string(&mut offset)?;
        let encryption_algorithms_server_to_client = buffer.read_byte_string(&mut offset)?;
        let mac_algorithms_client_to_server = buffer.read_byte_string(&mut offset)?;
        let mac_algorithms_server_to_client = buffer.read_byte_string(&mut offset)?;
        let compression_algorithms_client_to_server = buffer.read_byte_string(&mut offset)?;
        let compression_algorithms_server_to_client = buffer.read_byte_string(&mut offset)?;
        let languages_client_to_server = buffer.read_byte_string(&mut offset)?;
        let languages_server_to_client = buffer.read_byte_string(&mut offset)?;
        let first_kex_packet_follows = buffer.read_boolean(&mut offset)?;
        let reserved = buffer.read_uint32(&mut offset)?;

        Ok(Self {
            buffer,
            message_code,
            cookie,
            kex_algorithms,
            server_host_key_algorithms,
            encryption_algorithms_client_to_server,
            encryption_algorithms_server_to_client,
            mac_algorithms_client_to_server,
            mac_algorithms_server_to_client,
            compression_algorithms_client_to_server,
            compression_algorithms_server_to_client,
            languages_client_to_server,
            languages_server_to_client,
            first_kex_packet_follows,
            reserved,
        })
    }

    #[must_use]
    pub fn raw(&self) -> &[u8] {
        self.buffer
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::too_many_arguments, clippy::bool_assert_comparison)]

    use bstr::B;
    use rstest::rstest;

    use super::*;

    #[rustfmt::skip]
    #[rstest]
    #[case(
        "testdata/none-exec/02-client-kexinit.bin",
        SSH_MSG_KEXINIT,
        "00639d6c4d9c27c5bdc504f664cbc016",
        B("mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com"),
        B("ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256"),
        B("none"),
        B("none"),
        B("umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1"),
        B("umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1"),
        B("none,zlib@openssh.com"),
        B("none,zlib@openssh.com"),
        B(""),
        B(""),
        false,
        0
    )]
    fn read_file_works(
        #[case] packet_file: &str,
        #[case] message_code: u8,
        #[case] cookie: &str,
        #[case] kex_algorithms: &[u8],
        #[case] server_host_key_algorithms: &[u8],
        #[case] encryption_algorithms_client_to_server: &[u8],
        #[case] encryption_algorithms_server_to_client: &[u8],
        #[case] mac_algorithms_client_to_server: &[u8],
        #[case] mac_algorithms_server_to_client: &[u8],
        #[case] compression_algorithms_client_to_server: &[u8],
        #[case] compression_algorithms_server_to_client: &[u8],
        #[case] languages_client_to_server: &[u8],
        #[case] languages_server_to_client: &[u8],
        #[case] first_kex_packet_follows: bool,
        #[case] reserved: u32,
    ) {
        // Read the packet file and extract the payload
        let bytes = std::fs::read(packet_file).unwrap();
        let packet = crate::wire::Packet::new(&bytes, 0);
        let payload = packet.payload().unwrap();

        // Create a Kexinit view over the payload
        let kexinit = Kexinit::from_bytes(payload).unwrap();

        assert_eq!(kexinit.message_code, message_code);
        assert_eq!(kexinit.cookie, hex::decode(cookie).unwrap().as_array().unwrap());
        assert_eq!(kexinit.kex_algorithms, kex_algorithms);
        assert_eq!(kexinit.server_host_key_algorithms, server_host_key_algorithms);
        assert_eq!(kexinit.encryption_algorithms_client_to_server, encryption_algorithms_client_to_server);
        assert_eq!(kexinit.encryption_algorithms_server_to_client, encryption_algorithms_server_to_client);
        assert_eq!(kexinit.mac_algorithms_client_to_server, mac_algorithms_client_to_server);
        assert_eq!(kexinit.mac_algorithms_server_to_client, mac_algorithms_server_to_client);
        assert_eq!(kexinit.compression_algorithms_client_to_server, compression_algorithms_client_to_server);
        assert_eq!(kexinit.compression_algorithms_server_to_client, compression_algorithms_server_to_client);
        assert_eq!(kexinit.languages_client_to_server, languages_client_to_server);
        assert_eq!(kexinit.languages_server_to_client, languages_server_to_client);
        assert_eq!(kexinit.first_kex_packet_follows, first_kex_packet_follows);
        assert_eq!(kexinit.reserved, reserved);
    }

    // TODO: Move this test into roundtrip
    #[test]
    fn roundtrip_works() {
        // Create a builder with some test data
        let cookie = hex::decode("a28549776e59ce96ae73a58ca04224dc").unwrap();
        let cookie = cookie.as_array().unwrap();
        let kex_algs = b"curve25519-sha256,ecdh-sha2-nistp256";
        let host_key_algs = b"ssh-ed25519,rsa-sha2-256";
        let enc_c2s = b"aes128-ctr,aes256-ctr";
        let enc_s2c = b"aes128-ctr,aes256-ctr";
        let mac_c2s = b"hmac-sha2-256,hmac-sha2-512";
        let mac_s2c = b"hmac-sha2-256,hmac-sha2-512";
        let comp_c2s = b"none";
        let comp_s2c = b"none";
        let lang_c2s = b"";
        let lang_s2c = b"";

        let mut buffer = vec![0u8; 1024];

        Kexinit::write(
            &mut buffer,
            cookie,
            kex_algs,
            host_key_algs,
            enc_c2s,
            enc_s2c,
            mac_c2s,
            mac_s2c,
            comp_c2s,
            comp_s2c,
            lang_c2s,
            lang_s2c,
            false,
            0,
        )
        .unwrap();

        let kexinit = Kexinit::from_bytes(&buffer).unwrap();

        // Verify all fields match
        assert_eq!(kexinit.message_code, SSH_MSG_KEXINIT);
        assert_eq!(kexinit.cookie, cookie);
        assert_eq!(kexinit.kex_algorithms, kex_algs);
        assert_eq!(kexinit.server_host_key_algorithms, host_key_algs);
        assert_eq!(kexinit.encryption_algorithms_client_to_server, enc_c2s);
        assert_eq!(kexinit.encryption_algorithms_server_to_client, enc_s2c);
        assert_eq!(kexinit.mac_algorithms_client_to_server, mac_c2s);
        assert_eq!(kexinit.mac_algorithms_server_to_client, mac_s2c);
        assert_eq!(kexinit.compression_algorithms_client_to_server, comp_c2s);
        assert_eq!(kexinit.compression_algorithms_server_to_client, comp_s2c);
        assert_eq!(kexinit.languages_client_to_server, lang_c2s);
        assert_eq!(kexinit.languages_server_to_client, lang_s2c);
        assert_eq!(kexinit.first_kex_packet_follows, false);
        assert_eq!(kexinit.reserved, 0);
    }
}