use aead::{AeadInOut, 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 MAX_KEY_LEN: usize = 32;
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], out: &mut [u8]) -> Result<(), CryptoError> {
let hk = Hkdf::<Sha1>::new(Some(salt), master_key);
hk.expand(SS_SUBKEY_INFO, out)
.map_err(|_| CryptoError::Hkdf)
}
fn aead_seal_in_place(
cipher: Cipher,
subkey: &[u8],
buf: &mut [u8],
) -> Result<[u8; TAG_LEN], 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);
let tag = aead
.encrypt_inout_detached((&nonce).into(), b"", buf.into())
.map_err(|_| CryptoError::Aead)?;
let mut out = [0u8; TAG_LEN];
out.copy_from_slice(&tag);
Ok(out)
}};
}
match cipher {
Cipher::Aes128Gcm => seal!(Aes128Gcm),
Cipher::Aes256Gcm => seal!(Aes256Gcm),
Cipher::ChaCha20Poly1305 => seal!(ChaCha20Poly1305),
}
}
fn aead_open_in_place(
cipher: Cipher,
subkey: &[u8],
buf: &mut [u8],
tag: &[u8],
) -> Result<(), 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);
let tag = aead::Tag::<$alg>::try_from(tag).map_err(|_| CryptoError::Aead)?;
aead.decrypt_inout_detached((&nonce).into(), b"", buf.into(), &tag)
.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 datagram = Vec::with_capacity(salt_len + plaintext.len() + TAG_LEN);
datagram.resize(salt_len, 0);
rand::rng().fill(&mut datagram[..salt_len]);
let mut subkey = [0u8; MAX_KEY_LEN];
let subkey = &mut subkey[..cipher.key_len()];
derive_subkey(master_key, &datagram[..salt_len], subkey)?;
datagram.extend_from_slice(plaintext);
let tag = aead_seal_in_place(cipher, subkey, &mut datagram[salt_len..])?;
datagram.extend_from_slice(&tag);
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, rest) = datagram.split_at(salt_len);
let mut subkey = [0u8; MAX_KEY_LEN];
let subkey = &mut subkey[..cipher.key_len()];
derive_subkey(master_key, salt, subkey)?;
let (ciphertext, tag) = rest.split_at(rest.len() - TAG_LEN);
let mut plaintext = ciphertext.to_vec();
aead_open_in_place(cipher, subkey, &mut plaintext, tag)?;
Ok(plaintext)
}
#[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
}
}