use aead::{Aead, KeyInit};
use aes_gcm::{Aes128Gcm, Aes256Gcm};
use chacha20poly1305::ChaCha20Poly1305;
use hkdf::Hkdf;
use md5::{Digest, Md5};
use rand::RngExt;
use sha1::Sha1;
pub const NONCE_LEN: usize = 12;
pub const TAG_LEN: usize = 16;
const SS_SUBKEY_INFO: &[u8] = b"ss-subkey";
#[derive(Debug, thiserror::Error)]
pub enum CryptoError {
#[error("unknown cipher: {0}")]
UnknownCipher(String),
#[error("datagram too short: {got} bytes, need at least {need}")]
TooShort {
got: usize,
need: usize,
},
#[error("subkey derivation failed")]
Hkdf,
#[error("AEAD operation failed (authentication failure or bad key)")]
Aead,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cipher {
Aes128Gcm,
Aes256Gcm,
ChaCha20Poly1305,
}
impl Cipher {
pub fn from_name(name: &str) -> Result<Self, CryptoError> {
match name {
"aes-128-gcm" => Ok(Cipher::Aes128Gcm),
"aes-256-gcm" => Ok(Cipher::Aes256Gcm),
"chacha20-poly1305" | "chacha20-ietf-poly1305" => Ok(Cipher::ChaCha20Poly1305),
other => Err(CryptoError::UnknownCipher(other.to_string())),
}
}
pub fn name(self) -> &'static str {
match self {
Cipher::Aes128Gcm => "aes-128-gcm",
Cipher::Aes256Gcm => "aes-256-gcm",
Cipher::ChaCha20Poly1305 => "chacha20-poly1305",
}
}
pub fn key_len(self) -> usize {
match self {
Cipher::Aes128Gcm => 16,
Cipher::Aes256Gcm | Cipher::ChaCha20Poly1305 => 32,
}
}
pub fn salt_len(self) -> usize {
self.key_len()
}
}
pub fn evp_bytes_to_key(password: &[u8], key_len: usize) -> Vec<u8> {
let mut key = Vec::with_capacity(key_len);
let mut prev: Vec<u8> = Vec::new();
while key.len() < key_len {
let mut hasher = Md5::new();
hasher.update(&prev);
hasher.update(password);
prev = hasher.finalize().to_vec();
key.extend_from_slice(&prev);
}
key.truncate(key_len);
key
}
fn derive_subkey(master_key: &[u8], salt: &[u8], key_len: usize) -> Result<Vec<u8>, CryptoError> {
let hk = Hkdf::<Sha1>::new(Some(salt), master_key);
let mut subkey = vec![0u8; key_len];
hk.expand(SS_SUBKEY_INFO, &mut subkey)
.map_err(|_| CryptoError::Hkdf)?;
Ok(subkey)
}
fn aead_seal(cipher: Cipher, subkey: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
let nonce = [0u8; NONCE_LEN];
macro_rules! seal {
($alg:ty) => {{
let key = aead::Key::<$alg>::try_from(subkey).map_err(|_| CryptoError::Aead)?;
let aead = <$alg>::new(&key);
aead.encrypt((&nonce).into(), plaintext)
.map_err(|_| CryptoError::Aead)
}};
}
match cipher {
Cipher::Aes128Gcm => seal!(Aes128Gcm),
Cipher::Aes256Gcm => seal!(Aes256Gcm),
Cipher::ChaCha20Poly1305 => seal!(ChaCha20Poly1305),
}
}
fn aead_open(cipher: Cipher, subkey: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
let nonce = [0u8; NONCE_LEN];
macro_rules! open {
($alg:ty) => {{
let key = aead::Key::<$alg>::try_from(subkey).map_err(|_| CryptoError::Aead)?;
let aead = <$alg>::new(&key);
aead.decrypt((&nonce).into(), ciphertext)
.map_err(|_| CryptoError::Aead)
}};
}
match cipher {
Cipher::Aes128Gcm => open!(Aes128Gcm),
Cipher::Aes256Gcm => open!(Aes256Gcm),
Cipher::ChaCha20Poly1305 => open!(ChaCha20Poly1305),
}
}
pub fn encrypt_packet(
cipher: Cipher,
master_key: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let salt_len = cipher.salt_len();
let mut salt = vec![0u8; salt_len];
rand::rng().fill(salt.as_mut_slice());
let subkey = derive_subkey(master_key, &salt, cipher.key_len())?;
let ciphertext = aead_seal(cipher, &subkey, plaintext)?;
let mut datagram = Vec::with_capacity(salt_len + ciphertext.len());
datagram.extend_from_slice(&salt);
datagram.extend_from_slice(&ciphertext);
Ok(datagram)
}
pub fn decrypt_packet(
cipher: Cipher,
master_key: &[u8],
datagram: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let salt_len = cipher.salt_len();
let need = salt_len + TAG_LEN;
if datagram.len() < need {
return Err(CryptoError::TooShort {
got: datagram.len(),
need,
});
}
let (salt, ciphertext) = datagram.split_at(salt_len);
let subkey = derive_subkey(master_key, salt, cipher.key_len())?;
aead_open(cipher, &subkey, ciphertext)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evp_bytes_to_key_reference_vector() {
let key = evp_bytes_to_key(b"test", 16);
assert_eq!(hex_encode(&key), "098f6bcd4621d373cade4e832627b4f6");
}
#[test]
fn evp_bytes_to_key_32_byte_length() {
let key = evp_bytes_to_key(b"test", 32);
assert_eq!(key.len(), 32);
assert_eq!(hex_encode(&key[..16]), "098f6bcd4621d373cade4e832627b4f6");
}
#[test]
fn round_trip_all_ciphers() {
let plaintext = b"the raw IP packet bytes that traverse the tunnel";
for cipher in [
Cipher::Aes128Gcm,
Cipher::Aes256Gcm,
Cipher::ChaCha20Poly1305,
] {
let master_key = evp_bytes_to_key(b"correct horse battery staple", cipher.key_len());
let datagram = encrypt_packet(cipher, &master_key, plaintext).expect("encrypt");
assert_eq!(
datagram.len(),
cipher.salt_len() + plaintext.len() + TAG_LEN,
"datagram length for {}",
cipher.name()
);
let recovered = decrypt_packet(cipher, &master_key, &datagram).expect("decrypt");
assert_eq!(recovered, plaintext, "round trip for {}", cipher.name());
}
}
#[test]
fn round_trip_empty_plaintext() {
let cipher = Cipher::ChaCha20Poly1305;
let master_key = evp_bytes_to_key(b"pw", cipher.key_len());
let datagram = encrypt_packet(cipher, &master_key, b"").expect("encrypt");
let recovered = decrypt_packet(cipher, &master_key, &datagram).expect("decrypt");
assert!(recovered.is_empty());
}
#[test]
fn flipped_byte_is_rejected() {
let plaintext = b"authenticate me";
for cipher in [
Cipher::Aes128Gcm,
Cipher::Aes256Gcm,
Cipher::ChaCha20Poly1305,
] {
let master_key = evp_bytes_to_key(b"password", cipher.key_len());
let datagram = encrypt_packet(cipher, &master_key, plaintext).expect("encrypt");
let mut bad_salt = datagram.clone();
bad_salt[0] ^= 0xff;
assert!(
decrypt_packet(cipher, &master_key, &bad_salt).is_err(),
"flipped salt byte must be rejected for {}",
cipher.name()
);
let mut bad_ct = datagram.clone();
let last = bad_ct.len() - 1;
bad_ct[last] ^= 0x01;
assert!(
decrypt_packet(cipher, &master_key, &bad_ct).is_err(),
"flipped tag byte must be rejected for {}",
cipher.name()
);
}
}
#[test]
fn too_short_datagram_is_rejected() {
let cipher = Cipher::Aes128Gcm;
let master_key = evp_bytes_to_key(b"pw", cipher.key_len());
let short = vec![0u8; cipher.salt_len() + TAG_LEN - 1];
let err = decrypt_packet(cipher, &master_key, &short).unwrap_err();
assert!(matches!(err, CryptoError::TooShort { .. }));
}
#[test]
fn cipher_name_parsing() {
assert_eq!(Cipher::from_name("aes-128-gcm").unwrap(), Cipher::Aes128Gcm);
assert_eq!(Cipher::from_name("aes-256-gcm").unwrap(), Cipher::Aes256Gcm);
assert_eq!(
Cipher::from_name("chacha20-poly1305").unwrap(),
Cipher::ChaCha20Poly1305
);
assert_eq!(
Cipher::from_name("chacha20-ietf-poly1305").unwrap(),
Cipher::ChaCha20Poly1305
);
assert!(Cipher::from_name("rc4-md5").is_err());
assert_eq!(Cipher::Aes256Gcm.name(), "aes-256-gcm");
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
}