use hkdf::Hkdf;
use rand_core::{OsRng, RngCore};
use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
use crate::{
errors::CryptoError,
exchange::{
encryptor::Encryptor,
pair::{self, KEMPair},
prekey::SignedPrekey,
},
signatures::keypair::{self, PublicKeyBytes, SignerPair, VerifierPair, ViewOperations},
};
const PROTOCOL_LABEL: &[u8] = b"pq-msg/v2";
const SESSION_FORMAT_VERSION: u8 = 2;
const INITIATOR_TO_RESPONDER: u8 = 0;
const RESPONDER_TO_INITIATOR: u8 = 1;
const SESSION_BYTES: usize = 2
+ keypair::PUBLIC_KEY_BYTES
+ keypair::SECRET_KEY_BYTES
+ keypair::PUBLIC_KEY_BYTES
+ 32 * 3
+ 16
+ 8 * 2;
pub struct MessageSession {
ds_pair: SignerPair,
target_verifier: VerifierPair,
is_initiator: bool,
transcript: [u8; 32],
send_chain: Zeroizing<[u8; 32]>,
recv_chain: Zeroizing<[u8; 32]>,
session_id: [u8; 16],
send_counter: u64,
recv_counter: u64,
}
impl MessageSession {
pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
let mut bytes = Zeroizing::new(Vec::with_capacity(SESSION_BYTES));
bytes.push(SESSION_FORMAT_VERSION);
bytes.push(self.is_initiator as u8);
bytes.extend_from_slice(&self.ds_pair.to_bytes_uniform());
bytes.extend_from_slice(self.target_verifier.pub_key_bytes());
bytes.extend_from_slice(&self.transcript);
bytes.extend_from_slice(&self.send_chain[..]);
bytes.extend_from_slice(&self.recv_chain[..]);
bytes.extend_from_slice(&self.session_id);
bytes.extend_from_slice(&self.send_counter.to_le_bytes());
bytes.extend_from_slice(&self.recv_counter.to_le_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
if bytes.len() != SESSION_BYTES {
return Err(CryptoError::IncongruentLength(SESSION_BYTES, bytes.len()));
}
if bytes[0] != SESSION_FORMAT_VERSION || bytes[1] > 1 {
return Err(CryptoError::InvalidSession);
}
let mut rest = &bytes[2..];
let mut take = |n: usize| {
let (head, tail) = rest.split_at(n);
rest = tail;
head
};
Ok(Self {
is_initiator: bytes[1] == 1,
ds_pair: SignerPair::from_bytes_uniform(take(
keypair::PUBLIC_KEY_BYTES + keypair::SECRET_KEY_BYTES,
))?,
target_verifier: VerifierPair::new(take(keypair::PUBLIC_KEY_BYTES))?,
transcript: take(32).try_into()?,
send_chain: Zeroizing::new(take(32).try_into()?),
recv_chain: Zeroizing::new(take(32).try_into()?),
session_id: take(16).try_into()?,
send_counter: u64::from_le_bytes(take(8).try_into()?),
recv_counter: u64::from_le_bytes(take(8).try_into()?),
})
}
pub fn new_initiator(
my_signer: SignerPair,
base_nonce: [u8; 24],
target_prekey: &SignedPrekey,
target_verifier: &PublicKeyBytes,
) -> Result<(Self, [u8; pair::CIPHERTEXT_BYTES]), CryptoError> {
let target_verifier = VerifierPair::new(target_verifier)?;
if !target_prekey.verify(target_verifier.pub_key_bytes()) {
return Err(CryptoError::InvalidSignature);
}
let (shared_secret, ciphertext) = KEMPair::encapsulate(target_prekey.kem_pub_key())?;
let session = Self::establish(
my_signer,
target_verifier,
true,
base_nonce,
target_prekey.kem_pub_key(),
&ciphertext,
&shared_secret,
);
Ok((session, ciphertext))
}
pub fn new_responder(
my_prekey: &KEMPair,
my_signer: SignerPair,
base_nonce: [u8; 24],
ciphertext: &[u8; pair::CIPHERTEXT_BYTES],
sender_verifier: &PublicKeyBytes,
) -> Result<Self, CryptoError> {
let target_verifier = VerifierPair::new(sender_verifier)?;
let shared_secret = my_prekey.decapsulate(ciphertext);
Ok(Self::establish(
my_signer,
target_verifier,
false,
base_nonce,
&my_prekey.pub_key_bytes(),
ciphertext,
&shared_secret,
))
}
fn establish(
ds_pair: SignerPair,
target_verifier: VerifierPair,
is_initiator: bool,
base_nonce: [u8; 24],
responder_kem_pubkey: &[u8; pair::PUBLIC_KEY_BYTES],
ciphertext: &[u8; pair::CIPHERTEXT_BYTES],
shared_secret: &[u8; pair::SHARED_SECRET_BYTES],
) -> Self {
let (initiator, responder) = if is_initiator {
(ds_pair.pub_key_bytes(), target_verifier.pub_key_bytes())
} else {
(target_verifier.pub_key_bytes(), ds_pair.pub_key_bytes())
};
let transcript: [u8; 32] = Sha256::new()
.chain_update(PROTOCOL_LABEL)
.chain_update(base_nonce)
.chain_update(initiator)
.chain_update(responder)
.chain_update(responder_kem_pubkey)
.chain_update(ciphertext)
.finalize()
.into();
let hkdf = Hkdf::<Sha256>::new(Some(&transcript), shared_secret);
let mut i2r = Zeroizing::new([0u8; 32]);
let mut r2i = Zeroizing::new([0u8; 32]);
hkdf.expand(b"pq-msg/v2 initiator->responder", &mut i2r[..])
.expect("32 bytes is a valid HKDF-SHA256 output length");
hkdf.expand(b"pq-msg/v2 responder->initiator", &mut r2i[..])
.expect("32 bytes is a valid HKDF-SHA256 output length");
let (send_chain, recv_chain) = if is_initiator { (i2r, r2i) } else { (r2i, i2r) };
let counter = u64::from_le_bytes(base_nonce[16..].try_into().unwrap());
Self {
ds_pair,
target_verifier,
is_initiator,
transcript,
send_chain,
recv_chain,
session_id: base_nonce[..16].try_into().unwrap(),
send_counter: counter,
recv_counter: counter,
}
}
pub fn craft_message(&mut self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
let counter = self
.send_counter
.checked_add(1)
.ok_or(CryptoError::NonceExhausted)?;
let direction = self.send_direction();
let signed_data = self.signed_data(direction, counter, message);
let sig = self.ds_pair.sign(&signed_data)?;
let plaintext = Zeroizing::new([&sig[..], message].concat());
let (message_key, next_chain) = ratchet(&self.send_chain);
let ciphertext = Encryptor::new(&message_key)
.encrypt(&plaintext, &create_nonce(&self.session_id, counter))?;
self.send_chain = next_chain;
self.send_counter = counter;
Ok(ciphertext)
}
pub fn validate_message(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
let counter = self
.recv_counter
.checked_add(1)
.ok_or(CryptoError::NonceExhausted)?;
let direction = 1 - self.send_direction();
let (message_key, next_chain) = ratchet(&self.recv_chain);
let mut plaintext = Encryptor::new(&message_key)
.decrypt(ciphertext, &create_nonce(&self.session_id, counter))?;
if plaintext.len() < keypair::SIGNATURE_BYTES {
return Err(CryptoError::InvalidSignature);
}
let (sig, message) = plaintext.split_at(keypair::SIGNATURE_BYTES);
if !self
.target_verifier
.verify(&self.signed_data(direction, counter, message), sig)
{
return Err(CryptoError::InvalidSignature);
}
self.recv_chain = next_chain;
self.recv_counter = counter;
plaintext.drain(..keypair::SIGNATURE_BYTES);
Ok(plaintext)
}
fn send_direction(&self) -> u8 {
if self.is_initiator {
INITIATOR_TO_RESPONDER
} else {
RESPONDER_TO_INITIATOR
}
}
fn signed_data(&self, direction: u8, counter: u64, message: &[u8]) -> Vec<u8> {
[
&self.transcript[..],
&[direction],
&counter.to_le_bytes(),
message,
]
.concat()
}
pub fn send_counter(&self) -> u64 {
self.send_counter
}
pub fn recv_counter(&self) -> u64 {
self.recv_counter
}
}
fn ratchet(chain: &[u8; 32]) -> (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>) {
let hkdf = Hkdf::<Sha256>::from_prk(chain).expect("32 bytes is a valid HKDF-SHA256 PRK");
let mut message_key = Zeroizing::new([0u8; 32]);
let mut next_chain = Zeroizing::new([0u8; 32]);
hkdf.expand(b"pq-msg/v2 message key", &mut message_key[..])
.expect("32 bytes is a valid HKDF-SHA256 output length");
hkdf.expand(b"pq-msg/v2 chain key", &mut next_chain[..])
.expect("32 bytes is a valid HKDF-SHA256 output length");
(message_key, next_chain)
}
pub fn gen_session_id() -> [u8; 16] {
let mut session_id = [0u8; 16];
OsRng.fill_bytes(&mut session_id);
session_id
}
pub fn create_nonce(session_id: &[u8; 16], counter: u64) -> [u8; 24] {
let mut nonce = [0u8; 24];
nonce[..16].copy_from_slice(session_id);
nonce[16..24].copy_from_slice(&counter.to_le_bytes());
nonce
}
#[cfg(test)]
mod tests {
use super::*;
fn signed_prekey(owner: &mut SignerPair) -> (KEMPair, SignedPrekey) {
let prekey = KEMPair::create();
let signed = SignedPrekey::new(owner, &prekey).unwrap();
(prekey, signed)
}
fn session_pair(base_counter: u64) -> (MessageSession, MessageSession) {
let alice_ds = SignerPair::create();
let mut bob_ds = SignerPair::create();
let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
let base_nonce = create_nonce(&gen_session_id(), base_counter);
let (alice, ciphertext) = MessageSession::new_initiator(
alice_ds.clone(),
base_nonce,
&bob_signed_prekey,
bob_ds.pub_key_bytes(),
)
.unwrap();
let bob = MessageSession::new_responder(
&bob_prekey,
bob_ds,
base_nonce,
&ciphertext,
alice_ds.pub_key_bytes(),
)
.unwrap();
(alice, bob)
}
#[test]
fn test_message_session_serialization() {
let (mut alice, mut bob) = session_pair(0);
bob.validate_message(&alice.craft_message(b"before").unwrap())
.unwrap();
let mut restored = MessageSession::from_bytes(&alice.to_bytes()).unwrap();
assert_eq!(alice.to_bytes(), restored.to_bytes());
assert_eq!(restored.send_counter(), 1);
let msg = bob.validate_message(&restored.craft_message(b"after").unwrap());
assert_eq!(msg.unwrap(), b"after");
let reply = restored.validate_message(&bob.craft_message(b"reply").unwrap());
assert_eq!(reply.unwrap(), b"reply");
}
#[test]
fn test_session_deserialization_rejects_bad_input() {
let (alice, _) = session_pair(0);
let bytes = alice.to_bytes();
assert!(matches!(
MessageSession::from_bytes(&bytes[1..]),
Err(CryptoError::IncongruentLength(SESSION_BYTES, _))
));
let mut wrong_version = bytes.to_vec();
wrong_version[0] = 1;
assert!(matches!(
MessageSession::from_bytes(&wrong_version),
Err(CryptoError::InvalidSession)
));
let mut wrong_role = bytes.to_vec();
wrong_role[1] = 2;
assert!(matches!(
MessageSession::from_bytes(&wrong_role),
Err(CryptoError::InvalidSession)
));
let mut wrong_own_key = bytes.to_vec();
wrong_own_key[3] ^= 1;
assert!(matches!(
MessageSession::from_bytes(&wrong_own_key),
Err(CryptoError::InvalidKey)
));
let mut wrong_peer_key = bytes.to_vec();
wrong_peer_key[2 + keypair::PUBLIC_KEY_BYTES + keypair::SECRET_KEY_BYTES] ^= 0xFF;
assert!(matches!(
MessageSession::from_bytes(&wrong_peer_key),
Err(CryptoError::InvalidKey)
));
}
#[test]
fn test_wrong_peer_identity_rejected() {
let alice_ds = SignerPair::create();
let carol_ds = SignerPair::create();
let mut bob_ds = SignerPair::create();
let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
let base_nonce = create_nonce(&gen_session_id(), 0);
let (mut alice, ct) = MessageSession::new_initiator(
alice_ds,
base_nonce,
&bob_signed_prekey,
bob_ds.pub_key_bytes(),
)
.unwrap();
let mut bob = MessageSession::new_responder(
&bob_prekey,
bob_ds,
base_nonce,
&ct,
carol_ds.pub_key_bytes(),
)
.unwrap();
assert!(
bob.validate_message(&alice.craft_message(b"hi").unwrap())
.is_err()
);
}
#[test]
fn test_invalid_peer_keys_rejected() {
let mut bob_ds = SignerPair::create();
let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
let base_nonce = create_nonce(&gen_session_id(), 0);
let mut undecodable = *bob_ds.pub_key_bytes();
undecodable[0] ^= 0xFF;
let initiator = MessageSession::new_initiator(
SignerPair::create(),
base_nonce,
&bob_signed_prekey,
&undecodable,
);
assert!(matches!(initiator, Err(CryptoError::InvalidKey)));
let responder = MessageSession::new_responder(
&bob_prekey,
bob_ds,
base_nonce,
&[0u8; pair::CIPHERTEXT_BYTES],
&undecodable,
);
assert!(matches!(responder, Err(CryptoError::InvalidKey)));
}
#[test]
fn test_plaintext_too_short_for_signature() {
let (alice, mut bob) = session_pair(0);
let short = Encryptor::new(&ratchet(&alice.send_chain).0)
.encrypt(b"no signature here", &create_nonce(&alice.session_id, 1))
.unwrap();
assert!(matches!(
bob.validate_message(&short),
Err(CryptoError::InvalidSignature)
));
assert_eq!(bob.recv_counter(), 0);
}
#[test]
fn test_full_message_exchange() {
let (mut alice, mut bob) = session_pair(0);
assert_eq!(alice.send_chain, bob.recv_chain);
assert_eq!(alice.recv_chain, bob.send_chain);
assert_ne!(alice.send_chain, alice.recv_chain);
assert_eq!(alice.transcript, bob.transcript);
let message = b"Hello, Bob! This is a secret message.";
let encrypted_message = alice.craft_message(message).unwrap();
assert_eq!(alice.send_counter(), 1);
assert_eq!(bob.recv_counter(), 0);
let raw_message = bob.validate_message(&encrypted_message).unwrap();
assert_eq!(bob.recv_counter(), 1);
assert_eq!(raw_message, message);
let reply = b"Hello, Alice! I received your message safely.";
let encrypted_reply = bob.craft_message(reply).unwrap();
let raw_reply = alice.validate_message(&encrypted_reply).unwrap();
assert_eq!(raw_reply, reply);
assert_eq!(alice.send_counter(), bob.recv_counter());
assert_eq!(alice.recv_counter(), bob.send_counter());
}
#[test]
fn test_concurrent_sends() {
let (mut alice, mut bob) = session_pair(0);
let (ma, mb) = (
b"attack at dawn, bring snacks!!",
b"meet me at the north gate 9pm.",
);
let ca = alice.craft_message(ma).unwrap();
let cb = bob.craft_message(mb).unwrap();
let ct_xor: Vec<u8> = ca.iter().zip(&cb).map(|(x, y)| x ^ y).collect();
let pt_xor: Vec<u8> = ma.iter().zip(mb).map(|(x, y)| x ^ y).collect();
assert!(!ct_xor.windows(pt_xor.len()).any(|w| w == pt_xor));
assert_eq!(bob.validate_message(&ca).unwrap(), ma);
assert_eq!(alice.validate_message(&cb).unwrap(), mb);
let next = alice.craft_message(b"next").unwrap();
assert_eq!(bob.validate_message(&next).unwrap(), b"next");
}
#[test]
fn test_replay_and_garbage_do_not_desync() {
let (mut alice, mut bob) = session_pair(0);
let first = alice.craft_message(b"first").unwrap();
bob.validate_message(&first).unwrap();
assert!(bob.validate_message(&first).is_err());
assert!(bob.validate_message(&[0u8; 2000]).is_err());
assert_eq!(bob.recv_counter(), 1);
let second = alice.craft_message(b"second").unwrap();
assert_eq!(bob.validate_message(&second).unwrap(), b"second");
}
#[test]
fn test_out_of_order_rejected() {
let (mut alice, mut bob) = session_pair(0);
let first = alice.craft_message(b"first").unwrap();
let second = alice.craft_message(b"second").unwrap();
assert!(bob.validate_message(&second).is_err());
assert_eq!(bob.validate_message(&first).unwrap(), b"first");
assert_eq!(bob.validate_message(&second).unwrap(), b"second");
}
#[test]
fn test_reflected_message_rejected() {
let (mut alice, _) = session_pair(0);
let msg = alice.craft_message(b"hello").unwrap();
assert!(alice.validate_message(&msg).is_err());
}
#[test]
fn test_forwarded_message_rejected() {
let alice_ds = SignerPair::create();
let mut bob_ds = SignerPair::create();
let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
let mut carol_ds = SignerPair::create();
let (carol_prekey, carol_signed_prekey) = signed_prekey(&mut carol_ds);
let base_nonce = create_nonce(&gen_session_id(), 0);
let (mut alice, ct) = MessageSession::new_initiator(
alice_ds.clone(),
base_nonce,
&bob_signed_prekey,
bob_ds.pub_key_bytes(),
)
.unwrap();
let bob = MessageSession::new_responder(
&bob_prekey,
bob_ds.clone(),
base_nonce,
&ct,
alice_ds.pub_key_bytes(),
)
.unwrap();
let sent = alice.craft_message(b"I owe Bob 100 euros").unwrap();
let signed = Encryptor::new(&ratchet(&bob.recv_chain).0)
.decrypt(&sent, &create_nonce(&bob.session_id, 1))
.unwrap();
let (_, ct) = MessageSession::new_initiator(
bob_ds,
base_nonce,
&carol_signed_prekey,
carol_ds.pub_key_bytes(),
)
.unwrap();
let mut carol = MessageSession::new_responder(
&carol_prekey,
carol_ds,
base_nonce,
&ct,
alice_ds.pub_key_bytes(),
)
.unwrap();
let forged = Encryptor::new(&ratchet(&carol.recv_chain).0)
.encrypt(&signed, &create_nonce(&carol.session_id, 1))
.unwrap();
assert!(matches!(
carol.validate_message(&forged),
Err(CryptoError::InvalidSignature)
));
}
#[test]
fn test_stolen_session_cannot_read_past_messages() {
let (mut alice, mut bob) = session_pair(0);
let first = alice.craft_message(b"first").unwrap();
bob.validate_message(&first).unwrap();
let mut stolen = MessageSession::from_bytes(&bob.to_bytes()).unwrap();
stolen.recv_counter = 0;
assert!(stolen.validate_message(&first).is_err());
}
#[test]
fn test_deleted_prekey_gives_forward_secrecy() {
let alice_ds = SignerPair::create();
let mut bob_ds = SignerPair::create();
let (last_resort, _) = signed_prekey(&mut bob_ds);
let (one_time, one_time_signed) = signed_prekey(&mut bob_ds);
let base_nonce = create_nonce(&gen_session_id(), 0);
let (mut alice, ct) = MessageSession::new_initiator(
alice_ds.clone(),
base_nonce,
&one_time_signed,
bob_ds.pub_key_bytes(),
)
.unwrap();
let mut bob = MessageSession::new_responder(
&one_time,
bob_ds.clone(),
base_nonce,
&ct,
alice_ds.pub_key_bytes(),
)
.unwrap();
drop(one_time);
let recorded = alice.craft_message(b"secret").unwrap();
assert_eq!(bob.validate_message(&recorded).unwrap(), b"secret");
let mut attacker = MessageSession::new_responder(
&last_resort,
bob_ds,
base_nonce,
&ct,
alice_ds.pub_key_bytes(),
)
.unwrap();
assert!(attacker.validate_message(&recorded).is_err());
}
#[test]
fn test_initiator_rejects_prekey_not_signed_by_target() {
let bob_ds = SignerPair::create();
let mut mallory_ds = SignerPair::create();
let (_, fake) = signed_prekey(&mut mallory_ds);
let result = MessageSession::new_initiator(
SignerPair::create(),
create_nonce(&gen_session_id(), 0),
&fake,
bob_ds.pub_key_bytes(),
);
assert!(matches!(result, Err(CryptoError::InvalidSignature)));
}
#[test]
fn test_counter_exhaustion() {
let (mut alice, mut bob) = session_pair(u64::MAX - 1);
let last = alice.craft_message(b"last").unwrap();
assert_eq!(alice.send_counter(), u64::MAX);
assert_eq!(bob.validate_message(&last).unwrap(), b"last");
assert!(matches!(
alice.craft_message(b"one more"),
Err(CryptoError::NonceExhausted)
));
assert!(matches!(
bob.validate_message(&last),
Err(CryptoError::NonceExhausted)
));
}
#[test]
fn test_empty_message() {
let (mut alice, mut bob) = session_pair(0);
let encrypted = alice.craft_message(b"").unwrap();
assert_eq!(bob.validate_message(&encrypted).unwrap(), b"");
}
#[test]
fn test_session_ids_are_random() {
assert_ne!(gen_session_id(), gen_session_id());
}
#[test]
fn test_nonce_creation() {
let session_id = gen_session_id();
let counter = 5;
let nonce = create_nonce(&session_id, counter);
assert_eq!(&nonce[..16], &session_id[..]);
assert_eq!(&nonce[16..], &counter.to_le_bytes()[..]);
}
#[test]
fn test_base_counter() {
let (mut alice, mut bob) = session_pair(42);
assert_eq!(alice.send_counter(), 42);
assert_eq!(bob.recv_counter(), 42);
bob.validate_message(&alice.craft_message(b"hi").unwrap())
.unwrap();
assert_eq!(alice.send_counter(), 43);
assert_eq!(bob.recv_counter(), 43);
}
}