use crate::crypto::adaptive_crypto::CipherSuite;
use crate::crypto::kdf::derive_key_32;
use crate::errors::CoreError;
use zeroize::Zeroize;
pub const HP_MASK_LEN: usize = 15;
pub const HP_SAMPLE_LEN: usize = 16;
const HP_SEND_LABEL: &str = "phantom-hp-send-v1";
const HP_RECV_LABEL: &str = "phantom-hp-recv-v1";
pub struct HeaderProtector {
suite: CipherSuite,
hp_send: [u8; 32],
hp_recv: [u8; 32],
}
impl HeaderProtector {
pub fn derive(suite: CipherSuite, initial_secret: &[u8; 32], swap: bool) -> Self {
let hp_a = derive_key_32(HP_SEND_LABEL, initial_secret);
let hp_b = derive_key_32(HP_RECV_LABEL, initial_secret);
let (hp_send, hp_recv) = if swap { (hp_b, hp_a) } else { (hp_a, hp_b) };
Self {
suite,
hp_send,
hp_recv,
}
}
#[inline]
pub fn cipher_suite(&self) -> CipherSuite {
self.suite
}
#[inline]
pub fn mask_send(
&self,
sample: &[u8; HP_SAMPLE_LEN],
) -> Result<[u8; HP_SAMPLE_LEN], CoreError> {
compute_mask(self.suite, &self.hp_send, sample)
}
#[inline]
pub fn mask_recv(
&self,
sample: &[u8; HP_SAMPLE_LEN],
) -> Result<[u8; HP_SAMPLE_LEN], CoreError> {
compute_mask(self.suite, &self.hp_recv, sample)
}
}
impl Drop for HeaderProtector {
fn drop(&mut self) {
self.hp_send.zeroize();
self.hp_recv.zeroize();
}
}
#[inline]
fn compute_mask(
suite: CipherSuite,
key: &[u8; 32],
sample: &[u8; HP_SAMPLE_LEN],
) -> Result<[u8; HP_SAMPLE_LEN], CoreError> {
match suite {
CipherSuite::Aes256Gcm => aes256_ecb_block(key, sample),
CipherSuite::ChaCha20Poly1305 => chacha20_mask(key, sample),
}
}
#[cfg(not(feature = "fips"))]
fn aes256_ecb_block(key: &[u8; 32], sample: &[u8; 16]) -> Result<[u8; 16], CoreError> {
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockEncrypt, KeyInit};
let cipher = aes::Aes256::new(GenericArray::from_slice(key));
let mut block = *GenericArray::<u8, aes::cipher::consts::U16>::from_slice(sample);
cipher.encrypt_block(&mut block);
let mut out = [0u8; 16];
out.copy_from_slice(block.as_slice());
Ok(out)
}
#[cfg(feature = "fips")]
fn aes256_ecb_block(key: &[u8; 32], sample: &[u8; 16]) -> Result<[u8; 16], CoreError> {
use aws_lc_rs::cipher::{EncryptingKey, UnboundCipherKey, AES_256};
let unbound = UnboundCipherKey::new(&AES_256, key)
.map_err(|_| CoreError::CryptoError("HP: AES-256 key init failed".into()))?;
let enc = EncryptingKey::ecb(unbound)
.map_err(|_| CoreError::CryptoError("HP: AES-256-ECB init failed".into()))?;
let mut block = *sample;
enc.encrypt(&mut block)
.map_err(|_| CoreError::CryptoError("HP: AES-256-ECB encrypt failed".into()))?;
Ok(block)
}
fn chacha20_mask(key: &[u8; 32], sample: &[u8; 16]) -> Result<[u8; 16], CoreError> {
use chacha20::cipher::generic_array::GenericArray;
use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
let counter = u32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]);
let key_ga = GenericArray::from_slice(key);
let nonce_ga = GenericArray::from_slice(&sample[4..16]);
let mut cipher = chacha20::ChaCha20::new(key_ga, nonce_ga);
cipher.seek(u64::from(counter) * 64);
let mut out = [0u8; 16];
cipher.apply_keystream(&mut out);
Ok(out)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn hex16(s: &str) -> [u8; 16] {
let v = hex::decode(s).unwrap();
let mut a = [0u8; 16];
a.copy_from_slice(&v);
a
}
fn hex32(s: &str) -> [u8; 32] {
let v = hex::decode(s).unwrap();
let mut a = [0u8; 32];
a.copy_from_slice(&v);
a
}
#[test]
fn aes256_ecb_matches_nist_sp800_38a() {
let key = hex32("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4");
let pt = hex16("6bc1bee22e409f96e93d7e117393172a");
let ct = hex16("f3eed1bdb5d2a03c064b5a7e3db181f8");
assert_eq!(aes256_ecb_block(&key, &pt).unwrap(), ct);
}
#[cfg(not(feature = "fips"))]
#[test]
fn chacha20_mask_matches_rfc9001_a5() {
let key = hex32("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
let sample = hex16("5e5cd55c41f69080575d7999c25a5bfb");
let mask = chacha20_mask(&key, &sample).unwrap();
assert_eq!(&mask[..5], &[0xae, 0xfe, 0xfe, 0x7d, 0x03]);
}
#[test]
fn derive_swaps_send_recv_between_peers() {
let secret = [0x33u8; 32];
let client = HeaderProtector::derive(CipherSuite::Aes256Gcm, &secret, false);
let server = HeaderProtector::derive(CipherSuite::Aes256Gcm, &secret, true);
let sample = hex16("000102030405060708090a0b0c0d0e0f");
assert_eq!(
client.mask_send(&sample).unwrap(),
server.mask_recv(&sample).unwrap(),
"client send-mask must equal server recv-mask"
);
assert_eq!(
server.mask_send(&sample).unwrap(),
client.mask_recv(&sample).unwrap(),
"server send-mask must equal client recv-mask"
);
}
#[test]
fn send_and_recv_masks_differ() {
let secret = [0x77u8; 32];
let hp = HeaderProtector::derive(CipherSuite::Aes256Gcm, &secret, false);
let sample = hex16("0f0e0d0c0b0a09080706050403020100");
assert_ne!(
hp.mask_send(&sample).unwrap(),
hp.mask_recv(&sample).unwrap()
);
}
#[cfg(not(feature = "fips"))]
#[test]
fn chacha_suite_swap_is_consistent() {
let secret = [0x9au8; 32];
let client = HeaderProtector::derive(CipherSuite::ChaCha20Poly1305, &secret, false);
let server = HeaderProtector::derive(CipherSuite::ChaCha20Poly1305, &secret, true);
let sample = hex16("aabbccddeeff00112233445566778899");
assert_eq!(
client.mask_send(&sample).unwrap(),
server.mask_recv(&sample).unwrap()
);
}
}