Skip to main content

eggress_protocol_shadowsocks/
method.rs

1use hkdf::Hkdf;
2use sha1::Sha1;
3
4use crate::error::ShadowsocksError;
5
6/// Supported Shadowsocks AEAD cipher methods.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CipherMethod {
9    Aes128Gcm,
10    Aes192Gcm,
11    Aes256Gcm,
12    ChaCha20IetfPoly1305,
13}
14
15impl CipherMethod {
16    /// Parse a method name string into a CipherMethod.
17    pub fn parse_method(s: &str) -> Result<Self, ShadowsocksError> {
18        match s.to_lowercase().as_str() {
19            "aes-128-gcm" => Ok(CipherMethod::Aes128Gcm),
20            "aes-192-gcm" => Ok(CipherMethod::Aes192Gcm),
21            "aes-256-gcm" => Ok(CipherMethod::Aes256Gcm),
22            "chacha20-ietf-poly1305" => Ok(CipherMethod::ChaCha20IetfPoly1305),
23            _ => {
24                if is_legacy_method(s) {
25                    Err(ShadowsocksError::LegacyMethodUnsupported(s.to_string()))
26                } else {
27                    Err(ShadowsocksError::UnsupportedMethod(s.to_string()))
28                }
29            }
30        }
31    }
32
33    /// Key size in bytes.
34    pub fn key_size(&self) -> usize {
35        match self {
36            CipherMethod::Aes128Gcm => 16,
37            CipherMethod::Aes192Gcm => 24,
38            CipherMethod::Aes256Gcm => 32,
39            CipherMethod::ChaCha20IetfPoly1305 => 32,
40        }
41    }
42
43    /// Salt/IV size in bytes.
44    pub fn salt_size(&self) -> usize {
45        self.key_size()
46    }
47
48    /// Nonce size in bytes.
49    pub fn nonce_size(&self) -> usize {
50        12
51    }
52
53    /// Authentication tag size in bytes.
54    pub fn tag_size(&self) -> usize {
55        16
56    }
57
58    /// Derive an AEAD subkey from password and salt using HKDF-SHA1.
59    ///
60    /// Uses `EVP_BytesToKey(password)` as IKM (matching OpenSSL/shadowsocks-rust),
61    /// truncated to `key_size` bytes, then HKDF-SHA1 with info="ss-subkey" per SIP003.
62    pub fn derive_key(&self, password: &[u8], salt: &[u8]) -> Result<Vec<u8>, ShadowsocksError> {
63        let full_ikm = evp_bytes_to_key(password);
64        // shadowsocks-rust passes only key_size bytes as IKM to HKDF.
65        // evp_bytes_to_key produces 48 bytes; we must truncate to key_size
66        // to match the subkey derivation.
67        let ikm = &full_ikm[..self.key_size()];
68        let hk = Hkdf::<Sha1>::new(Some(salt), ikm);
69        let mut key = vec![0u8; self.key_size()];
70        hk.expand(b"ss-subkey", &mut key)
71            .map_err(|e| ShadowsocksError::Other(format!("HKDF expand failed: {e}")))?;
72        Ok(key)
73    }
74}
75
76/// Exact legacy method inventory exposed by pproxy 2.7.9's `cipher.py` and
77/// `cipherpy.py`. The names remain recognized even when the optional
78/// `legacy-crypto` feature is disabled so callers get a stable compatibility
79/// diagnostic instead of an unknown-method error.
80pub fn is_legacy_method(name: &str) -> bool {
81    let lower = name.to_ascii_lowercase();
82    let without_ota = lower.strip_suffix('!').unwrap_or(&lower);
83    let normalized = without_ota.strip_suffix("-py").unwrap_or(without_ota);
84    matches!(
85        normalized,
86        "table"
87            | "aes-128-ctr"
88            | "aes-192-ctr"
89            | "aes-256-ctr"
90            | "aes-128-cfb"
91            | "aes-192-cfb"
92            | "aes-256-cfb"
93            | "aes-128-cfb1"
94            | "aes-192-cfb1"
95            | "aes-256-cfb1"
96            | "aes-128-cfb8"
97            | "aes-192-cfb8"
98            | "aes-256-cfb8"
99            | "aes-128-ofb"
100            | "aes-192-ofb"
101            | "aes-256-ofb"
102            | "rc4"
103            | "rc4-md5"
104            | "chacha20"
105            | "chacha20-ietf"
106            | "xchacha20"
107            | "xchacha20-ietf"
108            | "salsa20"
109            | "xsalsa20"
110            | "bf-cfb"
111            | "cast5-cfb"
112            | "des-cfb"
113            | "camellia-128-cfb"
114            | "camellia-192-cfb"
115            | "camellia-256-cfb"
116            | "idea-cfb"
117            | "rc2-cfb"
118            | "seed-cfb"
119    )
120}
121
122impl std::fmt::Display for CipherMethod {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            CipherMethod::Aes128Gcm => write!(f, "aes-128-gcm"),
126            CipherMethod::Aes192Gcm => write!(f, "aes-192-gcm"),
127            CipherMethod::Aes256Gcm => write!(f, "aes-256-gcm"),
128            CipherMethod::ChaCha20IetfPoly1305 => write!(f, "chacha20-ietf-poly1305"),
129        }
130    }
131}
132
133/// OpenSSL `EVP_BytesToKey` with MD5 and no salt (type 0).
134///
135/// Used by Shadowsocks to derive the initial keying material from a password.
136/// Produces `d1 || d2 || ...` where `d1 = MD5(password)`, `d2 = MD5(d1 + password)`, etc.
137fn evp_bytes_to_key(password: &[u8]) -> Vec<u8> {
138    use md5::Digest as _;
139    use md5::Md5;
140
141    let mut key = Vec::new();
142    let mut prev = Vec::new();
143
144    // Produce enough key material (up to 48 bytes covers all AEAD key sizes)
145    while key.len() < 48 {
146        let mut hasher = Md5::new();
147        hasher.update(&prev);
148        hasher.update(password);
149        let digest = hasher.finalize();
150        prev = digest.to_vec();
151        key.extend_from_slice(&prev);
152    }
153
154    key
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_parse_aes_128_gcm() {
163        assert_eq!(
164            CipherMethod::parse_method("aes-128-gcm").unwrap(),
165            CipherMethod::Aes128Gcm
166        );
167        assert_eq!(
168            CipherMethod::parse_method("AES-128-GCM").unwrap(),
169            CipherMethod::Aes128Gcm
170        );
171    }
172
173    #[test]
174    fn test_parse_aes_256_gcm() {
175        assert_eq!(
176            CipherMethod::parse_method("aes-256-gcm").unwrap(),
177            CipherMethod::Aes256Gcm
178        );
179    }
180
181    #[test]
182    fn test_parse_aes_192_gcm() {
183        assert_eq!(
184            CipherMethod::parse_method("aes-192-gcm").unwrap(),
185            CipherMethod::Aes192Gcm
186        );
187    }
188
189    #[test]
190    fn test_parse_chacha20() {
191        assert_eq!(
192            CipherMethod::parse_method("chacha20-ietf-poly1305").unwrap(),
193            CipherMethod::ChaCha20IetfPoly1305
194        );
195    }
196
197    #[test]
198    fn test_parse_unknown() {
199        assert!(CipherMethod::parse_method("").is_err());
200    }
201
202    #[test]
203    fn test_legacy_method_detection() {
204        assert!(is_legacy_method("aes-128-ctr"));
205        assert!(is_legacy_method("aes-256-cfb"));
206        assert!(is_legacy_method("rc4"));
207        assert!(is_legacy_method("rc4-md5"));
208        assert!(is_legacy_method("RC4"));
209        assert!(!is_legacy_method("aes-128-gcm"));
210        assert!(!is_legacy_method("aes-256-gcm"));
211        assert!(!is_legacy_method("chacha20-ietf-poly1305"));
212        assert!(!is_legacy_method("totally-unknown"));
213    }
214
215    #[test]
216    fn test_parse_legacy_method_gives_legacy_error() {
217        match CipherMethod::parse_method("aes-128-ctr") {
218            Err(ShadowsocksError::LegacyMethodUnsupported(m)) => assert_eq!(m, "aes-128-ctr"),
219            other => panic!("expected LegacyMethodUnsupported, got {:?}", other),
220        }
221    }
222
223    #[test]
224    fn test_key_sizes() {
225        assert_eq!(CipherMethod::Aes128Gcm.key_size(), 16);
226        assert_eq!(CipherMethod::Aes192Gcm.key_size(), 24);
227        assert_eq!(CipherMethod::Aes256Gcm.key_size(), 32);
228        assert_eq!(CipherMethod::ChaCha20IetfPoly1305.key_size(), 32);
229    }
230
231    #[test]
232    fn test_salt_nonce_tag_sizes() {
233        for (method, size) in [
234            (CipherMethod::Aes128Gcm, 16),
235            (CipherMethod::Aes192Gcm, 24),
236            (CipherMethod::Aes256Gcm, 32),
237            (CipherMethod::ChaCha20IetfPoly1305, 32),
238        ] {
239            assert_eq!(method.salt_size(), size);
240            assert_eq!(method.nonce_size(), 12);
241            assert_eq!(method.tag_size(), 16);
242        }
243    }
244
245    #[test]
246    fn test_derive_key_deterministic() {
247        let method = CipherMethod::Aes256Gcm;
248        let password = b"test-password";
249        let salt = b"0123456789abcdef";
250        let key1 = method.derive_key(password, salt).unwrap();
251        let key2 = method.derive_key(password, salt).unwrap();
252        assert_eq!(key1, key2);
253        assert_eq!(key1.len(), 32);
254    }
255
256    #[test]
257    fn test_derive_key_different_salts() {
258        let method = CipherMethod::Aes256Gcm;
259        let password = b"test-password";
260        let key1 = method.derive_key(password, b"0000000000000000").unwrap();
261        let key2 = method.derive_key(password, b"1111111111111111").unwrap();
262        assert_ne!(key1, key2);
263    }
264
265    #[test]
266    fn test_display() {
267        assert_eq!(CipherMethod::Aes128Gcm.to_string(), "aes-128-gcm");
268        assert_eq!(CipherMethod::Aes192Gcm.to_string(), "aes-192-gcm");
269        assert_eq!(CipherMethod::Aes256Gcm.to_string(), "aes-256-gcm");
270        assert_eq!(
271            CipherMethod::ChaCha20IetfPoly1305.to_string(),
272            "chacha20-ietf-poly1305"
273        );
274    }
275
276    #[test]
277    fn test_evp_bytes_to_key() {
278        // Verified against Python's OpenSSL EVP_BytesToKey for "testpass"
279        let key = evp_bytes_to_key(b"testpass");
280        // First 16 bytes (for AES-128)
281        assert_eq!(
282            &key[..16],
283            &[
284                0x17, 0x9a, 0xd4, 0x5c, 0x6c, 0xe2, 0xcb, 0x97, 0xcf, 0x10, 0x29, 0xe2, 0x12, 0x04,
285                0x6e, 0x81
286            ]
287        );
288        // Full 32 bytes (for AES-256)
289        assert_eq!(
290            &key[..32],
291            &[
292                0x17, 0x9a, 0xd4, 0x5c, 0x6c, 0xe2, 0xcb, 0x97, 0xcf, 0x10, 0x29, 0xe2, 0x12, 0x04,
293                0x6e, 0x81, 0x9c, 0x8f, 0x2c, 0x70, 0x95, 0xd2, 0x8b, 0xf6, 0x24, 0xab, 0x97, 0x14,
294                0x3b, 0x51, 0xac, 0x4b
295            ]
296        );
297    }
298}