use std::fmt::Display;
use crate::{
connection_id::IdNonce,
hash::{HashAlgo, IncrementalHashState, Sha256},
};
use thiserror::Error;
use tokio_util::bytes::Bytes;
use uuid::Uuid;
#[derive(Debug, Error)]
#[error("invalid connection ID from server")]
pub struct ConnectionIdInvalid;
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct ConnectionId(Uuid);
impl ConnectionId {
pub fn new(client_nonce: &[u8], server_nonce: &[u8]) -> Self {
let mut h = Sha256::incremental();
h.update(server_nonce);
h.update(client_nonce);
let hash: [u8; 32] = h.finish().into_inner();
let hash: [u8; 16] = hash[..16]
.try_into()
.expect("infallible truncation from 32 bytes");
let uuid = Uuid::new_v8(hash);
Self(uuid)
}
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
}
impl Display for ConnectionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct UntrustedConnectionId {
server_nonce: Bytes,
id: Bytes,
}
impl UntrustedConnectionId {
pub fn new(server_nonce: Bytes, id: Bytes) -> Self {
Self { server_nonce, id }
}
pub fn verify(self, client_nonce: IdNonce) -> Result<ConnectionId, ConnectionIdInvalid> {
let derived = ConnectionId::new(client_nonce.as_bytes(), &self.server_nonce);
if derived.as_bytes() != &*self.id {
return Err(ConnectionIdInvalid);
}
Ok(derived)
}
}
#[cfg(test)]
mod tests {
use proptest::{prelude::*, strategy::LazyJust};
fn arbitrary_bytes() -> impl Strategy<Value = Bytes> {
prop::collection::vec(any::<u8>(), 0..1028).prop_map(Bytes::from)
}
fn arbitrary_nonce() -> impl Strategy<Value = IdNonce> {
LazyJust::new(IdNonce::default)
}
use super::*;
fn id_bytes_for(server_nonce: &Bytes, client_nonce: &IdNonce) -> [u8; 16] {
*ConnectionId::new(client_nonce.as_bytes(), server_nonce).as_bytes()
}
#[test]
fn test_fixture() {
let server_nonce: [u8; 16] = [42; 16];
let client_nonce: [u8; 16] = [13; 16];
let id = ConnectionId::new(&client_nonce, &server_nonce);
assert_eq!(id.to_string(), "dca7b886-dd81-8a2b-b3bb-bc3fd24da50e");
}
proptest! {
#[test]
fn prop_construction_from_untrusted(
client_nonce in arbitrary_nonce(),
server_nonce in arbitrary_bytes(),
) {
let expected = id_bytes_for(&server_nonce, &client_nonce);
let untrusted = UntrustedConnectionId::new(server_nonce.clone(), Bytes::copy_from_slice(&expected));
assert_eq!(untrusted.server_nonce, server_nonce);
assert_eq!(&*untrusted.id, expected);
let trusted = untrusted.verify(client_nonce).expect("valid inputs");
assert_eq!(*trusted.as_bytes(), expected);
}
#[test]
fn prop_incorrect_client_nonce(
client_nonce in arbitrary_nonce(),
server_nonce in arbitrary_bytes(),
attacker_nonce in arbitrary_nonce(),
) {
prop_assume!(client_nonce.as_bytes() != attacker_nonce.as_bytes());
let proposed_id = id_bytes_for(&server_nonce, &attacker_nonce);
let _: ConnectionIdInvalid =
UntrustedConnectionId::new(server_nonce, Bytes::from_owner(proposed_id))
.verify(client_nonce)
.expect_err("incorrect client ID must fail");
}
}
}