use aes_gcm::{
Aes256Gcm, KeyInit, Nonce as AesNonce,
aead::Aead,
};
use chacha20poly1305::XChaCha20Poly1305;
use rand::RngExt;
use x25519_dalek::{PublicKey, StaticSecret};
use crate::network::key_types::{X25519Pubkey, X25519Seckey};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncryptType {
AesGcm,
XChaCha20,
}
impl EncryptType {
pub fn as_str(&self) -> &'static str {
match self {
EncryptType::AesGcm => "aes-gcm",
EncryptType::XChaCha20 => "xchacha20",
}
}
}
impl std::fmt::Display for EncryptType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub fn parse_enc_type(s: &str) -> Result<EncryptType, HopEncryptionError> {
match s {
"xchacha20" | "xchacha20-poly1305" => Ok(EncryptType::XChaCha20),
"aes-gcm" | "gcm" => Ok(EncryptType::AesGcm),
_ => Err(HopEncryptionError::InvalidEncType(s.to_string())),
}
}
const GCM_IV_SIZE: usize = 16;
const GCM_TAG_SIZE: usize = 16;
const XCHACHA20_NONCE_SIZE: usize = 24;
const XCHACHA20_TAG_SIZE: usize = 16;
#[derive(Debug, thiserror::Error)]
pub enum HopEncryptionError {
#[error("Encryption failed: {0}")]
EncryptionFailed(String),
#[error("Decryption failed: {0}")]
DecryptionFailed(String),
#[error("Shared key derivation failed")]
SharedKeyFailed,
#[error("Ciphertext too short")]
CiphertextTooShort,
#[error("Invalid encryption type: {0}")]
InvalidEncType(String),
}
pub struct HopEncryption {
private_key: X25519Seckey,
public_key: X25519Pubkey,
server: bool,
}
impl HopEncryption {
pub fn new(private_key: X25519Seckey, public_key: X25519Pubkey, server: bool) -> Self {
Self {
private_key,
public_key,
server,
}
}
pub fn response_long_enough(enc_type: EncryptType, response_size: usize) -> bool {
match enc_type {
EncryptType::XChaCha20 => response_size >= XCHACHA20_TAG_SIZE,
EncryptType::AesGcm => response_size >= GCM_IV_SIZE + GCM_TAG_SIZE,
}
}
pub fn encrypt(
&self,
enc_type: EncryptType,
plaintext: &[u8],
remote_pubkey: &X25519Pubkey,
) -> Result<Vec<u8>, HopEncryptionError> {
match enc_type {
EncryptType::XChaCha20 => self.encrypt_xchacha20(plaintext, remote_pubkey),
EncryptType::AesGcm => self.encrypt_aesgcm(plaintext, remote_pubkey),
}
}
pub fn decrypt(
&self,
enc_type: EncryptType,
ciphertext: &[u8],
remote_pubkey: &X25519Pubkey,
) -> Result<Vec<u8>, HopEncryptionError> {
match enc_type {
EncryptType::XChaCha20 => self.decrypt_xchacha20(ciphertext, remote_pubkey),
EncryptType::AesGcm => self.decrypt_aesgcm(ciphertext, remote_pubkey),
}
}
fn encrypt_xchacha20(
&self,
plaintext: &[u8],
remote_pubkey: &X25519Pubkey,
) -> Result<Vec<u8>, HopEncryptionError> {
let key = xchacha20_shared_key(
&self.public_key,
&self.private_key,
remote_pubkey,
!self.server,
)?;
let mut nonce_bytes = [0u8; XCHACHA20_NONCE_SIZE];
rand::rng().fill(&mut nonce_bytes[..]);
let cipher = XChaCha20Poly1305::new(
chacha20poly1305::Key::from_slice(&key),
);
let nonce = chacha20poly1305::XNonce::from_slice(&nonce_bytes);
let encrypted = cipher
.encrypt(nonce, plaintext)
.map_err(|e| HopEncryptionError::EncryptionFailed(e.to_string()))?;
let mut result = Vec::with_capacity(XCHACHA20_NONCE_SIZE + encrypted.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&encrypted);
Ok(result)
}
fn decrypt_xchacha20(
&self,
ciphertext: &[u8],
remote_pubkey: &X25519Pubkey,
) -> Result<Vec<u8>, HopEncryptionError> {
if ciphertext.len() < XCHACHA20_NONCE_SIZE + XCHACHA20_TAG_SIZE {
return Err(HopEncryptionError::CiphertextTooShort);
}
let (nonce_bytes, encrypted) = ciphertext.split_at(XCHACHA20_NONCE_SIZE);
let key = xchacha20_shared_key(
&self.public_key,
&self.private_key,
remote_pubkey,
!self.server,
)?;
let cipher = XChaCha20Poly1305::new(
chacha20poly1305::Key::from_slice(&key),
);
let nonce = chacha20poly1305::XNonce::from_slice(nonce_bytes);
cipher
.decrypt(nonce, encrypted)
.map_err(|_| HopEncryptionError::DecryptionFailed("XChaCha20-Poly1305".into()))
}
fn encrypt_aesgcm(
&self,
plaintext: &[u8],
remote_pubkey: &X25519Pubkey,
) -> Result<Vec<u8>, HopEncryptionError> {
let key = derive_aesgcm_key(&self.private_key, remote_pubkey)?;
let mut iv = [0u8; GCM_IV_SIZE];
rand::rng().fill(&mut iv[..]);
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| HopEncryptionError::EncryptionFailed(e.to_string()))?;
let nonce = AesNonce::from_slice(&iv[..12]);
let encrypted = cipher
.encrypt(nonce, plaintext)
.map_err(|e| HopEncryptionError::EncryptionFailed(e.to_string()))?;
let mut result = Vec::with_capacity(GCM_IV_SIZE + encrypted.len());
result.extend_from_slice(&iv);
result.extend_from_slice(&encrypted);
Ok(result)
}
fn decrypt_aesgcm(
&self,
ciphertext: &[u8],
remote_pubkey: &X25519Pubkey,
) -> Result<Vec<u8>, HopEncryptionError> {
if ciphertext.len() < GCM_IV_SIZE + GCM_TAG_SIZE {
return Err(HopEncryptionError::CiphertextTooShort);
}
let key = derive_aesgcm_key(&self.private_key, remote_pubkey)?;
let (iv, encrypted) = ciphertext.split_at(GCM_IV_SIZE);
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| HopEncryptionError::DecryptionFailed(e.to_string()))?;
let nonce = AesNonce::from_slice(&iv[..12]);
cipher
.decrypt(nonce, encrypted)
.map_err(|_| HopEncryptionError::DecryptionFailed("AES256-GCM".into()))
}
}
fn dh(secret: &X25519Seckey, public: &X25519Pubkey) -> Result<[u8; 32], HopEncryptionError> {
let sk = StaticSecret::from(secret.0);
let pk = PublicKey::from(public.0);
let shared = sk.diffie_hellman(&pk);
Ok(shared.to_bytes())
}
fn xchacha20_shared_key(
local_pub: &X25519Pubkey,
local_sec: &X25519Seckey,
remote_pub: &X25519Pubkey,
local_first: bool,
) -> Result<[u8; 32], HopEncryptionError> {
let shared_secret = dh(local_sec, remote_pub)?;
let mut hasher = blake2b_simd::Params::new();
hasher.hash_length(32);
let mut state = hasher.to_state();
state.update(&shared_secret);
if local_first {
state.update(local_pub.as_bytes());
state.update(remote_pub.as_bytes());
} else {
state.update(remote_pub.as_bytes());
state.update(local_pub.as_bytes());
}
let hash = state.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(hash.as_bytes());
Ok(key)
}
fn derive_aesgcm_key(
secret: &X25519Seckey,
public: &X25519Pubkey,
) -> Result<[u8; 32], HopEncryptionError> {
use hmac::{Hmac, Mac};
use sha2_0_10::Sha256;
let shared_secret = dh(secret, public)?;
type HmacSha256 = Hmac<Sha256>;
let mut mac =
<HmacSha256 as Mac>::new_from_slice(b"LOKI")
.map_err(|e| HopEncryptionError::EncryptionFailed(e.to_string()))?;
Mac::update(&mut mac, &shared_secret);
let result = mac.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&result.into_bytes());
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::key_types::x25519_keypair;
#[test]
fn test_xchacha20_roundtrip() {
let (client_pk, client_sk) = x25519_keypair();
let (server_pk, server_sk) = x25519_keypair();
let client_enc = HopEncryption::new(client_sk, client_pk, false);
let server_enc = HopEncryption::new(server_sk, server_pk, true);
let plaintext = b"Hello, onion routing!";
let ciphertext = client_enc
.encrypt(EncryptType::XChaCha20, plaintext, &server_pk)
.unwrap();
let decrypted = server_enc
.decrypt(EncryptType::XChaCha20, &ciphertext, &client_pk)
.unwrap();
assert_eq!(&decrypted, plaintext);
}
#[test]
fn test_xchacha20_server_to_client() {
let (client_pk, client_sk) = x25519_keypair();
let (server_pk, server_sk) = x25519_keypair();
let client_enc = HopEncryption::new(client_sk, client_pk, false);
let server_enc = HopEncryption::new(server_sk, server_pk, true);
let plaintext = b"Response from server";
let ciphertext = server_enc
.encrypt(EncryptType::XChaCha20, plaintext, &client_pk)
.unwrap();
let decrypted = client_enc
.decrypt(EncryptType::XChaCha20, &ciphertext, &server_pk)
.unwrap();
assert_eq!(&decrypted, plaintext);
}
#[test]
fn test_aesgcm_roundtrip() {
let (client_pk, client_sk) = x25519_keypair();
let (server_pk, server_sk) = x25519_keypair();
let client_enc = HopEncryption::new(client_sk, client_pk, false);
let server_enc = HopEncryption::new(server_sk, server_pk, true);
let plaintext = b"Hello AES-GCM!";
let ciphertext = client_enc
.encrypt(EncryptType::AesGcm, plaintext, &server_pk)
.unwrap();
let decrypted = server_enc
.decrypt(EncryptType::AesGcm, &ciphertext, &client_pk)
.unwrap();
assert_eq!(&decrypted, plaintext);
}
#[test]
fn test_xchacha20_short_ciphertext() {
let (pk, sk) = x25519_keypair();
let (remote_pk, _) = x25519_keypair();
let enc = HopEncryption::new(sk, pk, false);
let result = enc.decrypt(EncryptType::XChaCha20, &[0u8; 5], &remote_pk);
assert!(result.is_err());
}
#[test]
fn test_parse_enc_type() {
assert_eq!(parse_enc_type("xchacha20").unwrap(), EncryptType::XChaCha20);
assert_eq!(
parse_enc_type("xchacha20-poly1305").unwrap(),
EncryptType::XChaCha20
);
assert_eq!(parse_enc_type("aes-gcm").unwrap(), EncryptType::AesGcm);
assert_eq!(parse_enc_type("gcm").unwrap(), EncryptType::AesGcm);
assert!(parse_enc_type("unknown").is_err());
}
#[test]
fn test_response_long_enough() {
assert!(!HopEncryption::response_long_enough(EncryptType::XChaCha20, 0));
assert!(HopEncryption::response_long_enough(EncryptType::XChaCha20, 16));
assert!(!HopEncryption::response_long_enough(EncryptType::AesGcm, 0));
assert!(HopEncryption::response_long_enough(EncryptType::AesGcm, 32));
}
#[test]
fn test_encrypt_type_display() {
assert_eq!(EncryptType::XChaCha20.as_str(), "xchacha20");
assert_eq!(EncryptType::AesGcm.as_str(), "aes-gcm");
}
}