eggress_protocol_shadowsocks/
method.rs1use hkdf::Hkdf;
2use sha1::Sha1;
3
4use crate::error::ShadowsocksError;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CipherMethod {
9 Aes128Gcm,
10 Aes192Gcm,
11 Aes256Gcm,
12 ChaCha20IetfPoly1305,
13}
14
15impl CipherMethod {
16 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 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 pub fn salt_size(&self) -> usize {
45 self.key_size()
46 }
47
48 pub fn nonce_size(&self) -> usize {
50 12
51 }
52
53 pub fn tag_size(&self) -> usize {
55 16
56 }
57
58 pub fn derive_key(&self, password: &[u8], salt: &[u8]) -> Result<Vec<u8>, ShadowsocksError> {
63 let full_ikm = evp_bytes_to_key(password);
64 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
76pub 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
133fn 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 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 let key = evp_bytes_to_key(b"testpass");
280 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 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}