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 password_key_material(password: &[u8]) -> Vec<u8> {
50 evp_bytes_to_key(password)
51 }
52
53 pub fn nonce_size(&self) -> usize {
55 12
56 }
57
58 pub fn tag_size(&self) -> usize {
60 16
61 }
62
63 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 pub fn derive_key_from_ikm(
73 &self,
74 full_ikm: &[u8],
75 salt: &[u8],
76 ) -> Result<Vec<u8>, ShadowsocksError> {
77 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
89pub 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
146fn 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 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 let key = evp_bytes_to_key(b"testpass");
293 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 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}