use alloc::vec::Vec;
use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
use hkdf::Hkdf;
use sha2::Sha256;
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::error::Error;
use crate::record::{RecordOpener, RecordSealer};
use crate::transcript::transcript_hash;
const LABEL_KEY_C2S: &[u8] = b"sealed-channel 1 key c2s";
const LABEL_KEY_S2C: &[u8] = b"sealed-channel 1 key s2c";
const LABEL_NONCE_C2S: &[u8] = b"sealed-channel 1 nonce c2s";
const LABEL_NONCE_S2C: &[u8] = b"sealed-channel 1 nonce s2c";
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SessionKeys {
c2s_key: [u8; 32],
s2c_key: [u8; 32],
c2s_nonce_prefix: [u8; 4],
s2c_nonce_prefix: [u8; 4],
}
pub fn derive(
psk: &[u8],
dh_shared_secret: &[u8; 32],
client_hello: &[u8],
server_challenge: &[u8],
) -> Result<SessionKeys, Error> {
let zero = [0u8; 32];
if bool::from(dh_shared_secret.ct_eq(&zero)) {
return Err(Error::WeakSharedSecret);
}
let transcript = transcript_hash(client_hello, server_challenge);
let mut ikm = Vec::with_capacity(32 + psk.len());
ikm.extend_from_slice(dh_shared_secret);
ikm.extend_from_slice(psk);
let hk = Hkdf::<Sha256>::new(Some(&transcript), &ikm);
let mut keys = SessionKeys {
c2s_key: [0u8; 32],
s2c_key: [0u8; 32],
c2s_nonce_prefix: [0u8; 4],
s2c_nonce_prefix: [0u8; 4],
};
hk.expand(LABEL_KEY_C2S, &mut keys.c2s_key)
.map_err(|_| Error::KeyDerivation)?;
hk.expand(LABEL_KEY_S2C, &mut keys.s2c_key)
.map_err(|_| Error::KeyDerivation)?;
hk.expand(LABEL_NONCE_C2S, &mut keys.c2s_nonce_prefix)
.map_err(|_| Error::KeyDerivation)?;
hk.expand(LABEL_NONCE_S2C, &mut keys.s2c_nonce_prefix)
.map_err(|_| Error::KeyDerivation)?;
ikm.zeroize();
Ok(keys)
}
impl SessionKeys {
pub fn into_client(mut self) -> (RecordSealer, RecordOpener) {
let sealer_cipher = ChaCha20Poly1305::new((&self.c2s_key).into());
let opener_cipher = ChaCha20Poly1305::new((&self.s2c_key).into());
let sealer = RecordSealer::new(sealer_cipher, self.c2s_nonce_prefix);
let opener = RecordOpener::new(opener_cipher, self.s2c_nonce_prefix);
self.c2s_key.zeroize();
self.s2c_key.zeroize();
(sealer, opener)
}
pub fn into_server(mut self) -> (RecordSealer, RecordOpener) {
let sealer_cipher = ChaCha20Poly1305::new((&self.s2c_key).into());
let opener_cipher = ChaCha20Poly1305::new((&self.c2s_key).into());
let sealer = RecordSealer::new(sealer_cipher, self.s2c_nonce_prefix);
let opener = RecordOpener::new(opener_cipher, self.c2s_nonce_prefix);
self.c2s_key.zeroize();
self.s2c_key.zeroize();
(sealer, opener)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_zero_dh_is_rejected() {
match derive(b"psk", &[0u8; 32], b"hello", b"challenge") {
Err(e) => assert_eq!(e, Error::WeakSharedSecret),
Ok(_) => panic!("all-zero DH must be rejected"),
}
}
#[test]
fn distinct_directions_have_distinct_keys() {
let keys = derive(b"a-good-psk", &[7u8; 32], b"hello", b"challenge").unwrap();
assert_ne!(keys.c2s_key, keys.s2c_key);
assert_ne!(keys.c2s_nonce_prefix, keys.s2c_nonce_prefix);
}
}