Skip to main content

io_proxy/socks/v5/
auth.rs

1//! SOCKS5 username/password sub-negotiation ([RFC 1929]).
2//!
3//! [RFC 1929]: https://www.rfc-editor.org/rfc/rfc1929
4
5use alloc::{
6    string::{String, ToString},
7    vec::Vec,
8};
9use core::fmt;
10
11use thiserror::Error;
12
13use crate::socks::v5::AUTH_VERSION;
14
15/// Failure building [`Socks5Credentials`].
16#[derive(Clone, Debug, Error, PartialEq, Eq)]
17pub enum Socks5CredentialsError {
18    /// The username exceeds the 255-byte field limit.
19    #[error("SOCKS5 username too long: {0} bytes (max 255)")]
20    UsernameTooLong(usize),
21    /// The password exceeds the 255-byte field limit.
22    #[error("SOCKS5 password too long: {0} bytes (max 255)")]
23    PasswordTooLong(usize),
24}
25
26/// RFC 1929 username/password credentials.
27///
28/// The password is redacted from the [`Debug`] output.
29#[derive(Clone)]
30pub struct Socks5Credentials {
31    username: String,
32    password: String,
33}
34
35impl Socks5Credentials {
36    /// Builds credentials, validating both fields against the 255-byte
37    /// limit RFC 1929 imposes on each.
38    pub fn new(
39        username: &str,
40        password: &str,
41    ) -> Result<Socks5Credentials, Socks5CredentialsError> {
42        if username.len() > 255 {
43            return Err(Socks5CredentialsError::UsernameTooLong(username.len()));
44        }
45        if password.len() > 255 {
46            return Err(Socks5CredentialsError::PasswordTooLong(password.len()));
47        }
48        Ok(Socks5Credentials {
49            username: username.to_string(),
50            password: password.to_string(),
51        })
52    }
53
54    /// Encodes the sub-negotiation request:
55    /// `VER(0x01) | ULEN | UNAME | PLEN | PASSWD`.
56    pub(crate) fn encode(&self) -> Vec<u8> {
57        let user = self.username.as_bytes();
58        let pass = self.password.as_bytes();
59
60        let mut out = Vec::with_capacity(3 + user.len() + pass.len());
61        out.push(AUTH_VERSION);
62        out.push(user.len() as u8);
63        out.extend_from_slice(user);
64        out.push(pass.len() as u8);
65        out.extend_from_slice(pass);
66        out
67    }
68}
69
70impl fmt::Debug for Socks5Credentials {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.debug_struct("Socks5Credentials")
73            .field("username", &self.username)
74            .field("password", &"***")
75            .finish()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn encode_matches_rfc1929() {
85        let creds = Socks5Credentials::new("user", "pass").unwrap();
86        // VER=1, ULEN=4, "user", PLEN=4, "pass"
87        assert_eq!(
88            creds.encode(),
89            [
90                0x01, 0x04, b'u', b's', b'e', b'r', 0x04, b'p', b'a', b's', b's'
91            ]
92        );
93    }
94
95    #[test]
96    fn rejects_overlong_fields() {
97        let long = "x".repeat(256);
98        assert!(matches!(
99            Socks5Credentials::new(&long, "p"),
100            Err(Socks5CredentialsError::UsernameTooLong(256))
101        ));
102        assert!(matches!(
103            Socks5Credentials::new("u", &long),
104            Err(Socks5CredentialsError::PasswordTooLong(256))
105        ));
106    }
107
108    #[test]
109    fn debug_redacts_password() {
110        let creds = Socks5Credentials::new("alice", "secret").unwrap();
111        let rendered = format!("{creds:?}");
112        assert!(rendered.contains("alice"));
113        assert!(!rendered.contains("secret"));
114    }
115}