use blake3::Hasher;
use chacha20poly1305::{
aead::{Aead, KeyInit, OsRng},
ChaCha20Poly1305, Key as ChachaKey, Nonce,
};
use hmac::Hmac;
use rand::RngCore;
use sha2::Sha256;
use subtle::ConstantTimeEq;
use x25519_dalek;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::error::{Error, Result};
pub const TAG_SIZE: usize = 8;
pub const X25519_PUBLIC_KEY_SIZE: usize = 32;
pub const X25519_PRIVATE_KEY_SIZE: usize = 32;
pub const CHACHA20_KEY_SIZE: usize = 32;
pub const POLY1305_TAG_SIZE: usize = 16;
pub const NONCE_SIZE: usize = 12;
pub const DEFAULT_WINDOW_MS: u64 = 10_000;
const HKDF_SESSION_KEY_CONTEXT: &str = "aivpn-session-key-v1";
const HKDF_TAG_SECRET_CONTEXT: &str = "aivpn-tag-secret-v1";
const HKDF_PRNG_SEED_CONTEXT: &str = "aivpn-prng-seed-v1";
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SessionKeys {
pub session_key: [u8; CHACHA20_KEY_SIZE],
pub tag_secret: [u8; 32],
pub prng_seed: [u8; 32],
}
#[derive(Debug, Clone)]
pub struct KeyPair {
private_key_bytes: [u8; X25519_PRIVATE_KEY_SIZE],
public_key_bytes: [u8; X25519_PUBLIC_KEY_SIZE],
}
impl Drop for KeyPair {
fn drop(&mut self) {
self.private_key_bytes.zeroize();
}
}
impl KeyPair {
pub fn generate() -> Self {
let mut private_key_bytes = [0u8; 32];
OsRng.fill_bytes(&mut private_key_bytes);
private_key_bytes[0] &= 248;
private_key_bytes[31] &= 127;
private_key_bytes[31] |= 64;
let public_key_bytes =
x25519_dalek::x25519(private_key_bytes, x25519_dalek::X25519_BASEPOINT_BYTES);
Self {
private_key_bytes,
public_key_bytes,
}
}
pub fn from_private_key(mut key_bytes: [u8; 32]) -> Self {
key_bytes[0] &= 248;
key_bytes[31] &= 127;
key_bytes[31] |= 64;
let public_key_bytes =
x25519_dalek::x25519(key_bytes, x25519_dalek::X25519_BASEPOINT_BYTES);
Self {
private_key_bytes: key_bytes,
public_key_bytes,
}
}
pub fn public_key_bytes(&self) -> [u8; X25519_PUBLIC_KEY_SIZE] {
self.public_key_bytes
}
pub fn compute_shared(&self, remote_public: &[u8; X25519_PUBLIC_KEY_SIZE]) -> Result<[u8; 32]> {
let shared = x25519_dalek::x25519(self.private_key_bytes, *remote_public);
if shared.ct_eq(&[0u8; 32]).into() {
return Err(Error::Crypto(
"DH result is all-zero (possible small subgroup attack)".into(),
));
}
Ok(shared)
}
}
pub fn derive_session_keys(
dh_result: &[u8; 32],
preshared_key: Option<&[u8; 32]>,
eph_pub: &[u8; X25519_PUBLIC_KEY_SIZE],
) -> SessionKeys {
let ikm: Vec<u8> = if let Some(psk) = preshared_key {
let mut buf = [0u8; 64];
buf[..32].copy_from_slice(dh_result);
buf[32..].copy_from_slice(psk);
buf.to_vec()
} else {
dh_result.to_vec()
};
let session_key_input: Vec<u8> = [ikm.clone(), eph_pub.to_vec()].concat();
let tag_secret_input: Vec<u8> = [ikm.clone(), eph_pub.to_vec()].concat();
let prng_seed_input: Vec<u8> = [ikm, eph_pub.to_vec()].concat();
let session_key_hash = blake3::derive_key(HKDF_SESSION_KEY_CONTEXT, &session_key_input);
let tag_secret_hash = blake3::derive_key(HKDF_TAG_SECRET_CONTEXT, &tag_secret_input);
let prng_seed_hash = blake3::derive_key(HKDF_PRNG_SEED_CONTEXT, &prng_seed_input);
SessionKeys {
session_key: session_key_hash[..CHACHA20_KEY_SIZE].try_into().unwrap(),
tag_secret: tag_secret_hash[..32].try_into().unwrap(),
prng_seed: prng_seed_hash[..32].try_into().unwrap(),
}
}
pub fn encrypt_payload(
key: &[u8; CHACHA20_KEY_SIZE],
nonce: &[u8; NONCE_SIZE],
plaintext: &[u8],
) -> Result<Vec<u8>> {
let cipher = ChaCha20Poly1305::new(ChachaKey::from_slice(key));
let nonce = Nonce::from_slice(nonce);
let ciphertext = cipher.encrypt(nonce, plaintext)?;
Ok(ciphertext)
}
pub fn decrypt_payload(
key: &[u8; CHACHA20_KEY_SIZE],
nonce: &[u8; NONCE_SIZE],
ciphertext: &[u8],
) -> Result<Vec<u8>> {
let cipher = ChaCha20Poly1305::new(ChachaKey::from_slice(key));
let nonce = Nonce::from_slice(nonce);
let plaintext = cipher.decrypt(nonce, ciphertext)?;
Ok(plaintext)
}
pub fn generate_resonance_tag(
tag_secret: &[u8; 32],
counter: u64,
time_window: u64,
) -> [u8; TAG_SIZE] {
let mut hasher = Hasher::new_keyed(tag_secret);
hasher.update(&counter.to_le_bytes());
hasher.update(&time_window.to_le_bytes());
let hash = hasher.finalize();
let mut tag = [0u8; TAG_SIZE];
tag.copy_from_slice(&hash.as_bytes()[..TAG_SIZE]);
if tag[0] >= 1 && tag[0] <= 4 {
tag[0] = tag[0].wrapping_add(5); }
tag
}
pub fn compute_time_window(timestamp_ms: u64, window_ms: u64) -> u64 {
timestamp_ms / window_ms
}
pub fn current_timestamp_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
pub fn random_bytes(len: usize) -> Vec<u8> {
let mut buf = vec![0u8; len];
OsRng.fill_bytes(&mut buf);
buf
}
pub fn blake3_hash(data: &[u8]) -> [u8; 32] {
blake3::hash(data).into()
}
pub fn obfuscate_eph_pub(eph_pub: &mut [u8; 32], server_static_pub: &[u8; 32]) {
let mask = blake3::derive_key("aivpn-eph-obfuscation-v1", server_static_pub);
for i in 0..32 {
eph_pub[i] ^= mask[i];
}
}
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
use hmac::Mac;
type HmacSha256 = Hmac<Sha256>;
let mut mac = <HmacSha256 as Mac>::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data);
let result = mac.finalize();
result.into_bytes().into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_exchange() {
let client_keys = KeyPair::generate();
let server_keys = KeyPair::generate();
let client_shared = client_keys
.compute_shared(&server_keys.public_key_bytes())
.unwrap();
let server_shared = server_keys
.compute_shared(&client_keys.public_key_bytes())
.unwrap();
assert_eq!(client_shared, server_shared);
}
#[test]
fn test_encrypt_decrypt() {
let key = [1u8; CHACHA20_KEY_SIZE];
let nonce = [2u8; NONCE_SIZE];
let plaintext = b"Hello, AIVPN!";
let ciphertext = encrypt_payload(&key, &nonce, plaintext).unwrap();
let decrypted = decrypt_payload(&key, &nonce, &ciphertext).unwrap();
assert_eq!(plaintext.to_vec(), decrypted);
}
#[test]
fn test_resonance_tag() {
let tag_secret = [3u8; 32];
let tag1 = generate_resonance_tag(&tag_secret, 1, 100);
let tag2 = generate_resonance_tag(&tag_secret, 2, 100);
let tag3 = generate_resonance_tag(&tag_secret, 1, 100);
assert_ne!(tag1, tag2); assert_eq!(tag1, tag3); }
}