use hkdf::Hkdf;
use sha1::Sha1;
use crate::error::ShadowsocksError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CipherMethod {
Aes128Gcm,
Aes192Gcm,
Aes256Gcm,
ChaCha20IetfPoly1305,
}
impl CipherMethod {
pub fn parse_method(s: &str) -> Result<Self, ShadowsocksError> {
match s.to_lowercase().as_str() {
"aes-128-gcm" => Ok(CipherMethod::Aes128Gcm),
"aes-192-gcm" => Ok(CipherMethod::Aes192Gcm),
"aes-256-gcm" => Ok(CipherMethod::Aes256Gcm),
"chacha20-ietf-poly1305" => Ok(CipherMethod::ChaCha20IetfPoly1305),
_ => {
if is_legacy_method(s) {
Err(ShadowsocksError::LegacyMethodUnsupported(s.to_string()))
} else {
Err(ShadowsocksError::UnsupportedMethod(s.to_string()))
}
}
}
}
pub fn key_size(&self) -> usize {
match self {
CipherMethod::Aes128Gcm => 16,
CipherMethod::Aes192Gcm => 24,
CipherMethod::Aes256Gcm => 32,
CipherMethod::ChaCha20IetfPoly1305 => 32,
}
}
pub fn salt_size(&self) -> usize {
self.key_size()
}
pub fn nonce_size(&self) -> usize {
12
}
pub fn tag_size(&self) -> usize {
16
}
pub fn derive_key(&self, password: &[u8], salt: &[u8]) -> Result<Vec<u8>, ShadowsocksError> {
let full_ikm = evp_bytes_to_key(password);
let ikm = &full_ikm[..self.key_size()];
let hk = Hkdf::<Sha1>::new(Some(salt), ikm);
let mut key = vec![0u8; self.key_size()];
hk.expand(b"ss-subkey", &mut key)
.map_err(|e| ShadowsocksError::Other(format!("HKDF expand failed: {e}")))?;
Ok(key)
}
}
pub fn is_legacy_method(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
let without_ota = lower.strip_suffix('!').unwrap_or(&lower);
let normalized = without_ota.strip_suffix("-py").unwrap_or(without_ota);
matches!(
normalized,
"table"
| "aes-128-ctr"
| "aes-192-ctr"
| "aes-256-ctr"
| "aes-128-cfb"
| "aes-192-cfb"
| "aes-256-cfb"
| "aes-128-cfb1"
| "aes-192-cfb1"
| "aes-256-cfb1"
| "aes-128-cfb8"
| "aes-192-cfb8"
| "aes-256-cfb8"
| "aes-128-ofb"
| "aes-192-ofb"
| "aes-256-ofb"
| "rc4"
| "rc4-md5"
| "chacha20"
| "chacha20-ietf"
| "xchacha20"
| "xchacha20-ietf"
| "salsa20"
| "xsalsa20"
| "bf-cfb"
| "cast5-cfb"
| "des-cfb"
| "camellia-128-cfb"
| "camellia-192-cfb"
| "camellia-256-cfb"
| "idea-cfb"
| "rc2-cfb"
| "seed-cfb"
)
}
impl std::fmt::Display for CipherMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CipherMethod::Aes128Gcm => write!(f, "aes-128-gcm"),
CipherMethod::Aes192Gcm => write!(f, "aes-192-gcm"),
CipherMethod::Aes256Gcm => write!(f, "aes-256-gcm"),
CipherMethod::ChaCha20IetfPoly1305 => write!(f, "chacha20-ietf-poly1305"),
}
}
}
fn evp_bytes_to_key(password: &[u8]) -> Vec<u8> {
use md5::Digest as _;
use md5::Md5;
let mut key = Vec::new();
let mut prev = Vec::new();
while key.len() < 48 {
let mut hasher = Md5::new();
hasher.update(&prev);
hasher.update(password);
let digest = hasher.finalize();
prev = digest.to_vec();
key.extend_from_slice(&prev);
}
key
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_aes_128_gcm() {
assert_eq!(
CipherMethod::parse_method("aes-128-gcm").unwrap(),
CipherMethod::Aes128Gcm
);
assert_eq!(
CipherMethod::parse_method("AES-128-GCM").unwrap(),
CipherMethod::Aes128Gcm
);
}
#[test]
fn test_parse_aes_256_gcm() {
assert_eq!(
CipherMethod::parse_method("aes-256-gcm").unwrap(),
CipherMethod::Aes256Gcm
);
}
#[test]
fn test_parse_aes_192_gcm() {
assert_eq!(
CipherMethod::parse_method("aes-192-gcm").unwrap(),
CipherMethod::Aes192Gcm
);
}
#[test]
fn test_parse_chacha20() {
assert_eq!(
CipherMethod::parse_method("chacha20-ietf-poly1305").unwrap(),
CipherMethod::ChaCha20IetfPoly1305
);
}
#[test]
fn test_parse_unknown() {
assert!(CipherMethod::parse_method("").is_err());
}
#[test]
fn test_legacy_method_detection() {
assert!(is_legacy_method("aes-128-ctr"));
assert!(is_legacy_method("aes-256-cfb"));
assert!(is_legacy_method("rc4"));
assert!(is_legacy_method("rc4-md5"));
assert!(is_legacy_method("RC4"));
assert!(!is_legacy_method("aes-128-gcm"));
assert!(!is_legacy_method("aes-256-gcm"));
assert!(!is_legacy_method("chacha20-ietf-poly1305"));
assert!(!is_legacy_method("totally-unknown"));
}
#[test]
fn test_parse_legacy_method_gives_legacy_error() {
match CipherMethod::parse_method("aes-128-ctr") {
Err(ShadowsocksError::LegacyMethodUnsupported(m)) => assert_eq!(m, "aes-128-ctr"),
other => panic!("expected LegacyMethodUnsupported, got {:?}", other),
}
}
#[test]
fn test_key_sizes() {
assert_eq!(CipherMethod::Aes128Gcm.key_size(), 16);
assert_eq!(CipherMethod::Aes192Gcm.key_size(), 24);
assert_eq!(CipherMethod::Aes256Gcm.key_size(), 32);
assert_eq!(CipherMethod::ChaCha20IetfPoly1305.key_size(), 32);
}
#[test]
fn test_salt_nonce_tag_sizes() {
for (method, size) in [
(CipherMethod::Aes128Gcm, 16),
(CipherMethod::Aes192Gcm, 24),
(CipherMethod::Aes256Gcm, 32),
(CipherMethod::ChaCha20IetfPoly1305, 32),
] {
assert_eq!(method.salt_size(), size);
assert_eq!(method.nonce_size(), 12);
assert_eq!(method.tag_size(), 16);
}
}
#[test]
fn test_derive_key_deterministic() {
let method = CipherMethod::Aes256Gcm;
let password = b"test-password";
let salt = b"0123456789abcdef";
let key1 = method.derive_key(password, salt).unwrap();
let key2 = method.derive_key(password, salt).unwrap();
assert_eq!(key1, key2);
assert_eq!(key1.len(), 32);
}
#[test]
fn test_derive_key_different_salts() {
let method = CipherMethod::Aes256Gcm;
let password = b"test-password";
let key1 = method.derive_key(password, b"0000000000000000").unwrap();
let key2 = method.derive_key(password, b"1111111111111111").unwrap();
assert_ne!(key1, key2);
}
#[test]
fn test_display() {
assert_eq!(CipherMethod::Aes128Gcm.to_string(), "aes-128-gcm");
assert_eq!(CipherMethod::Aes192Gcm.to_string(), "aes-192-gcm");
assert_eq!(CipherMethod::Aes256Gcm.to_string(), "aes-256-gcm");
assert_eq!(
CipherMethod::ChaCha20IetfPoly1305.to_string(),
"chacha20-ietf-poly1305"
);
}
#[test]
fn test_evp_bytes_to_key() {
let key = evp_bytes_to_key(b"testpass");
assert_eq!(
&key[..16],
&[
0x17, 0x9a, 0xd4, 0x5c, 0x6c, 0xe2, 0xcb, 0x97, 0xcf, 0x10, 0x29, 0xe2, 0x12, 0x04,
0x6e, 0x81
]
);
assert_eq!(
&key[..32],
&[
0x17, 0x9a, 0xd4, 0x5c, 0x6c, 0xe2, 0xcb, 0x97, 0xcf, 0x10, 0x29, 0xe2, 0x12, 0x04,
0x6e, 0x81, 0x9c, 0x8f, 0x2c, 0x70, 0x95, 0xd2, 0x8b, 0xf6, 0x24, 0xab, 0x97, 0x14,
0x3b, 0x51, 0xac, 0x4b
]
);
}
}