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