use crate::Result;
use crate::wg::constants::{
BLAKE2S_128_SIZE, COOKIE_REFRESH_TIME, MESSAGE_COOKIE_REPLY_SIZE, MESSAGE_COOKIE_REPLY_TYPE,
NoisePublicKey,
};
use crate::wg::crypto::{
blake2s_mac_128, calculate_cookie_key, calculate_mac1_key, ct_eq, fill_random, xaead_open,
xaead_seal,
};
use std::time::Instant;
pub(crate) struct CookieChecker {
mac1_key: [u8; 32],
secret: [u8; 32],
secret_set: Instant,
encryption_key: [u8; 32],
}
impl CookieChecker {
pub fn new(local_pub: &NoisePublicKey) -> CookieChecker {
let mut secret = [0u8; 32];
let _ = fill_random(&mut secret);
CookieChecker {
mac1_key: calculate_mac1_key(local_pub),
secret,
secret_set: Instant::now(),
encryption_key: calculate_cookie_key(local_pub),
}
}
fn maybe_refresh(&mut self) {
if self.secret_set.elapsed() > COOKIE_REFRESH_TIME {
let _ = fill_random(&mut self.secret);
self.secret_set = Instant::now();
}
}
pub fn check_mac1(&self, msg: &[u8]) -> bool {
if msg.len() < BLAKE2S_128_SIZE * 2 {
return false;
}
let smac2 = msg.len() - BLAKE2S_128_SIZE;
let smac1 = smac2 - BLAKE2S_128_SIZE;
let computed = blake2s_mac_128(&self.mac1_key, &msg[..smac1]);
ct_eq(&computed, &msg[smac1..smac2])
}
pub fn check_mac2(&self, msg: &[u8], src: &[u8]) -> bool {
if self.secret_set.elapsed() > COOKIE_REFRESH_TIME {
return false;
}
if msg.len() < BLAKE2S_128_SIZE {
return false;
}
let cookie = blake2s_mac_128(&self.secret, src);
let smac2 = msg.len() - BLAKE2S_128_SIZE;
let computed = blake2s_mac_128(&cookie, &msg[..smac2]);
ct_eq(&computed, &msg[smac2..])
}
pub fn generate_reply(
&mut self,
src: &[u8],
receiver_idx: u32,
init_mac1: &[u8],
) -> Result<Vec<u8>> {
self.maybe_refresh();
let mut msg = vec![0u8; MESSAGE_COOKIE_REPLY_SIZE];
msg[0..4].copy_from_slice(&MESSAGE_COOKIE_REPLY_TYPE.to_le_bytes());
msg[4..8].copy_from_slice(&receiver_idx.to_le_bytes());
fill_random(&mut msg[8..32])?;
let cookie = blake2s_mac_128(&self.secret, src);
let mut nonce = [0u8; 24];
nonce.copy_from_slice(&msg[8..32]);
let enc = xaead_seal(&self.encryption_key, &nonce, &cookie, init_mac1);
msg[32..].copy_from_slice(&enc);
Ok(msg)
}
}
pub(crate) struct CookieGenerator {
mac1_key: [u8; 32],
encryption_key: [u8; 32],
cookie: [u8; BLAKE2S_128_SIZE],
cookie_set: Option<Instant>,
last_mac1: [u8; BLAKE2S_128_SIZE],
has_last_mac1: bool,
}
impl CookieGenerator {
pub fn new(remote_pub: &NoisePublicKey) -> CookieGenerator {
CookieGenerator {
mac1_key: calculate_mac1_key(remote_pub),
encryption_key: calculate_cookie_key(remote_pub),
cookie: [0u8; BLAKE2S_128_SIZE],
cookie_set: None,
last_mac1: [0u8; BLAKE2S_128_SIZE],
has_last_mac1: false,
}
}
pub fn add_macs(&mut self, msg: &mut [u8]) {
let size = msg.len();
if size < BLAKE2S_128_SIZE * 2 {
return;
}
let smac2 = size - BLAKE2S_128_SIZE;
let smac1 = smac2 - BLAKE2S_128_SIZE;
let mac1 = blake2s_mac_128(&self.mac1_key, &msg[..smac1]);
msg[smac1..smac2].copy_from_slice(&mac1);
self.last_mac1.copy_from_slice(&mac1);
self.has_last_mac1 = true;
let fresh = self
.cookie_set
.map(|t| t.elapsed() <= COOKIE_REFRESH_TIME)
.unwrap_or(false);
if !fresh {
for b in &mut msg[smac2..] {
*b = 0;
}
return;
}
let mac2 = blake2s_mac_128(&self.cookie, &msg[..smac2]);
msg[smac2..].copy_from_slice(&mac2);
}
pub fn consume_reply(&mut self, nonce: &[u8; 24], ct: &[u8]) -> Result<()> {
if !self.has_last_mac1 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"cookie reply with no prior initiation",
));
}
let pt = xaead_open(&self.encryption_key, nonce, ct, &self.last_mac1)?;
if pt.len() != BLAKE2S_128_SIZE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"decrypted cookie wrong size",
));
}
self.cookie.copy_from_slice(&pt);
self.cookie_set = Some(Instant::now());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wg::crypto::generate_private_key;
use crate::wg::crypto::x25519_public;
fn keypair() -> NoisePublicKey {
let priv_k = generate_private_key().unwrap();
x25519_public(&priv_k)
}
#[test]
fn mac1_matches_between_checker_and_generator() {
let responder = keypair();
let checker = CookieChecker::new(&responder);
let mut generator = CookieGenerator::new(&responder);
let mut pkt = vec![7u8; 148];
generator.add_macs(&mut pkt);
assert!(checker.check_mac1(&pkt));
pkt[0] ^= 0xFF;
assert!(!checker.check_mac1(&pkt));
}
#[test]
fn cookie_reply_roundtrip_enables_mac2() {
let responder = keypair();
let mut checker = CookieChecker::new(&responder);
let mut generator = CookieGenerator::new(&responder);
let src = [10u8, 0, 0, 9];
let mut pkt = vec![3u8; 148];
generator.add_macs(&mut pkt);
let init_mac1 = pkt[116..132].to_vec();
let reply = checker.generate_reply(&src, 0x1234, &init_mac1).unwrap();
assert_eq!(reply.len(), MESSAGE_COOKIE_REPLY_SIZE);
assert_eq!(
u32::from_le_bytes(reply[0..4].try_into().unwrap()),
MESSAGE_COOKIE_REPLY_TYPE
);
let mut nonce = [0u8; 24];
nonce.copy_from_slice(&reply[8..32]);
generator.consume_reply(&nonce, &reply[32..]).unwrap();
let mut pkt2 = vec![5u8; 148];
generator.add_macs(&mut pkt2);
assert!(checker.check_mac1(&pkt2));
assert!(checker.check_mac2(&pkt2, &src));
assert!(!checker.check_mac2(&pkt2, &[10, 0, 0, 10]));
}
#[test]
fn consume_reply_without_initiation_fails() {
let responder = keypair();
let mut generator = CookieGenerator::new(&responder);
let nonce = [0u8; 24];
assert!(generator.consume_reply(&nonce, &[0u8; 32]).is_err());
}
}