From b13bd527f8e708cdccdaa71d1b45b91e01af52c6 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 11 Feb 2026 15:59:35 -0500 Subject: [PATCH 01/16] Store public key in SecStream --- src/cipher.rs | 11 +++++---- src/state_machine.rs | 52 ++++++++++++++++++++++++++++++++--------- tests/js_integration.rs | 2 +- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index 58f29ed..95e740d 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -10,6 +10,7 @@ use std::{ use crypto_secretstream::Tag; use futures::{Sink, Stream}; +use snow::Keypair; use tracing::{instrument, trace, warn}; use crate::{ @@ -368,18 +369,18 @@ impl Cipher { /// Create a new responder from a private key pub fn resp_from_private( io: Option>>, - private: &[u8], + keypair: &Keypair, ) -> Result { - Self::resp_from_private_with_prologue(io, private, &[]) + Self::resp_from_private_with_prologue(io, keypair, &[]) } /// Create a new responder from a private key with a prologue pub fn resp_from_private_with_prologue( io: Option>>, - private: &[u8], + keypair: &Keypair, prologue: &[u8], ) -> Result { - let ss = SecStream::new_responder_with_prologue(private, prologue)?; + let ss = SecStream::new_responder_with_prologue(keypair, prologue)?; let state = State::RespStart(ss); let inner = SansIoCipher::new(state); Ok(Self::new(io, inner)) @@ -759,7 +760,7 @@ mod tests { ) { let kp = hc_specific::generate_keypair().unwrap(); let ssi = SecStream::new_initiator(&kp.public.clone().try_into().unwrap(), &[]).unwrap(); - let ssr = SecStream::new_responder(&kp.private).unwrap(); + let ssr = SecStream::new_responder(&kp).unwrap(); (kp, (ssi, ssr)) } diff --git a/src/state_machine.rs b/src/state_machine.rs index 20f4242..36b97af 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -8,8 +8,8 @@ //! let kp: snow::Keypair = generate_keypair()?; //! // Create an initiator and responder //! let init: SecStream> = -//! SecStream::new_initiator(&kp.public.try_into().unwrap(), &[])?; -//! let resp: SecStream> = SecStream::new_responder(&kp.private)?; +//! SecStream::new_initiator(&kp.public.clone().try_into().unwrap(), &[])?; +//! let resp: SecStream> = SecStream::new_responder(&kp)?; //! //! // initiator sends the first handshake message, a payload can be included to send extra data to the //! // responder. @@ -45,7 +45,7 @@ use crypto_secretstream::{Header, Key, PullStream, PushStream, Tag}; use rand::rngs::OsRng; -use snow::HandshakeState; +use snow::{HandshakeState, Keypair}; use std::{fmt::Debug, marker::PhantomData}; use tracing::error; @@ -56,12 +56,14 @@ use crate::{Error, crypto::write_stream_id}; const STREAM_ID_LENGTH: usize = 32; const RAW_HEADER_MSG_LEN: usize = STREAM_ID_LENGTH + Header::BYTES; const SNOW_CIPHERKEYLEN: usize = 32; -pub(crate) const PUBLIC_KEYLEN: usize = 32; +/// Length in bytes of a public key +pub const PUBLIC_KEYLEN: usize = 32; /// Secret Stream protocol state pub struct SecStream { is_initiator: bool, state: HandshakeState, + local_public_key: [u8; PUBLIC_KEYLEN], msg_buf: [u8; 1024], step: Step, } @@ -107,10 +109,17 @@ pub struct Initiator { /// The second is after it reads it and gets the payload, but before creating the encyptor and /// emitting the next message. This distinction is necessary so we can handle the received payload /// and send a new one -#[derive(Debug)] pub struct Responder { _res_step: PhantomData, } + +impl Debug for Responder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Responder") + .field("step", &self._res_step) + .finish() + } +} /// The first step. We must send or receive a handshake message to proceed. #[derive(Debug)] pub struct Start; @@ -214,6 +223,10 @@ impl SecStream> { Ok(Self { is_initiator: true, state, + local_public_key: key_pair + .public + .try_into() + .expect("Wrong sized key from snow?"), msg_buf: [0; 1024], step: Initiator { _res_step: PhantomData, @@ -232,12 +245,14 @@ impl SecStream> { is_initiator, state, msg_buf, + local_public_key, .. } = self; Ok(( SecStream { is_initiator, state, + local_public_key, msg_buf, step: Initiator { _res_step: PhantomData, @@ -250,19 +265,24 @@ impl SecStream> { impl SecStream> { /// Create a responder of a secret stream - pub fn new_responder(private: &[u8]) -> Result { - Self::new_responder_with_prologue(private, &[]) + pub fn new_responder(keypair: &Keypair) -> Result { + Self::new_responder_with_prologue(keypair, &[]) } /// Create a responder of a secret stream with a prologue - pub fn new_responder_with_prologue(private: &[u8], prologue: &[u8]) -> Result { + pub fn new_responder_with_prologue(keypair: &Keypair, prologue: &[u8]) -> Result { let state = hc_specific::builder() .prologue(prologue)? - .local_private_key(private)? + .local_private_key(&keypair.private)? .build_responder()?; Ok(Self { is_initiator: false, state, + local_public_key: keypair + .public + .clone() + .try_into() + .expect("Wrong sized key from snow?"), msg_buf: [0; 1024], step: Responder { _res_step: PhantomData, @@ -281,12 +301,14 @@ impl SecStream> { is_initiator, state, msg_buf, + local_public_key, .. } = self; Ok(( SecStream { is_initiator, state, + local_public_key, msg_buf, step: Responder { _res_step: PhantomData, @@ -336,12 +358,14 @@ impl SecStream> { is_initiator, state, msg_buf, + local_public_key, .. } = self; Ok(( SecStream { is_initiator, state, + local_public_key, msg_buf, step: EncryptorReady { rx: Key::from(rx), @@ -365,6 +389,7 @@ impl SecStream> { let Self { is_initiator, state, + local_public_key, msg_buf, .. } = self; @@ -372,6 +397,7 @@ impl SecStream> { SecStream { is_initiator, state, + local_public_key, msg_buf, step: Initiator { _res_step: PhantomData, @@ -416,6 +442,7 @@ impl SecStream> { let SecStream { is_initiator, state, + local_public_key, msg_buf, .. } = self; @@ -423,6 +450,7 @@ impl SecStream> { SecStream { is_initiator, state, + local_public_key, msg_buf, step: EncryptorReady { pusher, @@ -447,14 +475,15 @@ impl SecStream { pub fn read_msg(self, msg: &[u8]) -> Result, Error> { let Self { is_initiator, + state, + local_public_key, + msg_buf, step: EncryptorReady { pusher, rx, handshake_hash, }, - state, - msg_buf, } = self; // Read the received message from the other peer let mut expected_stream_id: [u8; STREAM_ID_LENGTH] = [0; STREAM_ID_LENGTH]; @@ -472,6 +501,7 @@ impl SecStream { Ok(SecStream { is_initiator, state, + local_public_key, msg_buf, step: Ready { pusher, diff --git a/tests/js_integration.rs b/tests/js_integration.rs index 1d71b4c..79a8503 100644 --- a/tests/js_integration.rs +++ b/tests/js_integration.rs @@ -97,7 +97,7 @@ async fn setup_js_initiator() -> Result<(Repl, Cipher)> { // Setup Cipher here let framed = Uint24LELengthPrefixedFraming::new(tcp.compat()); - let resp = SecStream::new_responder(&kp.private)?; + let resp = SecStream::new_responder(&kp)?; let cipher = Cipher::new_resp(Box::new(framed), resp); Ok::<_, Error>(cipher) }; From 2db4b0df9bce029475f7b29c2c33838f53ba3737 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 11 Feb 2026 16:11:00 -0500 Subject: [PATCH 02/16] Add Cipher.get_local_public_key --- src/cipher.rs | 21 +++++++++++++++++++++ src/state_machine.rs | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/src/cipher.rs b/src/cipher.rs index 95e740d..382c80f 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -54,6 +54,17 @@ impl State { Self::Invalid => None, } } + /// Get the local public key. + fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + Some(match self { + State::InitiatorStart(s) => s.get_local_public_key(), + State::InitiatorSent(s) => s.get_local_public_key(), + State::RespStart(s) => s.get_local_public_key(), + State::EncReady(s) => s.get_local_public_key(), + State::Ready(s) => s.get_local_public_key(), + State::Invalid => return None, + }) + } /// Get the handshake hash if available (only in Ready state). fn handshake_hash(&self) -> Option<&[u8]> { @@ -263,6 +274,11 @@ impl SansIoCipher { self.state.get_remote_static() } + /// Get the local public key. It is only unavailable when we are in [`State::Invalid`]. + fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + self.state.get_local_public_key() + } + /// Get the handshake hash if available (only after handshake completes). fn handshake_hash(&self) -> Option<&[u8]> { self.state.handshake_hash() @@ -531,6 +547,11 @@ impl Cipher { self.inner.get_remote_static() } + /// Get the local public key. It is only unavailable when we are in [`State::Invalid`]. + pub fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + self.inner.get_local_public_key() + } + /// Get the handshake hash. /// /// This is a unique identifier for this encrypted session, the same on both sides. diff --git a/src/state_machine.rs b/src/state_machine.rs index 36b97af..b871ed1 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -84,6 +84,11 @@ impl SecStream { if self.is_initiator { (a, b) } else { (b, a) } } + /// Get the local public key. + pub fn get_local_public_key(&self) -> [u8; PUBLIC_KEYLEN] { + self.local_public_key + } + /// Get the remote peer's static public key. /// /// For Responders this is `None` until processing reading the first handshake message From 89ab8b348f91fcba60db716c343f84e1639e8e0d Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 12 Feb 2026 15:16:13 -0500 Subject: [PATCH 03/16] Add XX pattern paramaterize via pattern --- src/cipher.rs | 518 ++++++++++++++++++++++++---- src/error.rs | 6 + src/lib.rs | 1 + src/state_machine.rs | 719 +++++++++++++++++++++++++++++++++++---- tests/test_xx_pattern.rs | 143 ++++++++ 5 files changed, 1258 insertions(+), 129 deletions(-) create mode 100644 tests/test_xx_pattern.rs diff --git a/src/cipher.rs b/src/cipher.rs index 382c80f..860b0ba 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -14,16 +14,36 @@ use snow::Keypair; use tracing::{instrument, trace, warn}; use crate::{ - Error, + Error, HandshakePattern, IK, XX, state_machine::{ - EncryptorReady, HsMsgSent, Initiator, PUBLIC_KEYLEN, Ready, Responder, SecStream, Start, + EncryptorReady, HsDone, HsMsgSent, Initiator, InitiatorXxFinalMsg, PUBLIC_KEYLEN, Ready, + Responder, ResponderXxAwaitingFinal, ResponderXxReceivedFirst, SecStream, Start, }, }; pub(crate) enum State { - InitiatorStart(SecStream>), - InitiatorSent(SecStream>), - RespStart(SecStream>), + // IK Initiator states + InitiatorIkStart(SecStream>), + InitiatorIkSent(SecStream>), + InitiatorIkHsDone(SecStream>), + + // IK Responder states + RespIkStart(SecStream>), + RespIkHsDone(SecStream>), + + // XX Initiator states + InitiatorXxStart(SecStream>), + InitiatorXxSent(SecStream>), + InitiatorXxFinalMsg(SecStream>), + InitiatorXxHsDone(SecStream>), + + // XX Responder states + RespXxStart(SecStream>), + RespXxReceivedFirst(SecStream>), + RespXxAwaitingFinal(SecStream>), + RespXxHsDone(SecStream>), + + // Common states (pattern-agnostic) EncReady(SecStream), Ready(SecStream), Invalid, @@ -32,11 +52,27 @@ pub(crate) enum State { impl Debug for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::InitiatorStart(arg0) => f.debug_tuple("InitiatorStart").field(arg0).finish(), - Self::InitiatorSent(arg0) => f.debug_tuple("InitiatorSent").field(arg0).finish(), - Self::RespStart(arg0) => f.debug_tuple("RespStart").field(arg0).finish(), - Self::EncReady(arg0) => f.debug_tuple("EncReady").field(arg0).finish(), - Self::Ready(arg0) => f.debug_tuple("Ready").field(arg0).finish(), + // IK pattern - Initiator + Self::InitiatorIkStart(s) => f.debug_tuple("InitiatorIkStart").field(s).finish(), + Self::InitiatorIkSent(s) => f.debug_tuple("InitiatorIkSent").field(s).finish(), + Self::InitiatorIkHsDone(s) => f.debug_tuple("InitiatorIkHsDone").field(s).finish(), + // IK pattern - Responder + Self::RespIkStart(s) => f.debug_tuple("RespIkStart").field(s).finish(), + Self::RespIkHsDone(s) => f.debug_tuple("RespIkHsDone").field(s).finish(), + + // XX pattern - Initiator + Self::InitiatorXxStart(s) => f.debug_tuple("InitiatorXxStart").field(s).finish(), + Self::InitiatorXxSent(s) => f.debug_tuple("InitiatorXxSent").field(s).finish(), + Self::InitiatorXxFinalMsg(s) => f.debug_tuple("InitiatorXxFinalMsg").field(s).finish(), + Self::InitiatorXxHsDone(s) => f.debug_tuple("InitiatorXxHsDone").field(s).finish(), + // XX pattern - Responder + Self::RespXxStart(s) => f.debug_tuple("RespXxStart").field(s).finish(), + Self::RespXxReceivedFirst(s) => f.debug_tuple("RespXxReceivedFirst").field(s).finish(), + Self::RespXxAwaitingFinal(s) => f.debug_tuple("RespXxAwaitingFinal").field(s).finish(), + Self::RespXxHsDone(s) => f.debug_tuple("RespXxHsDone").field(s).finish(), + Self::EncReady(s) => f.debug_tuple("EncReady").field(s).finish(), + Self::Ready(s) => f.debug_tuple("Ready").field(s).finish(), + // Bad Self::Invalid => write!(f, "Invalid"), } } @@ -46,9 +82,19 @@ impl State { /// Get the remote peer's static public key if available. fn get_remote_static(&self) -> Option<[u8; PUBLIC_KEYLEN]> { match self { - Self::InitiatorStart(s) => s.get_remote_static(), - Self::InitiatorSent(s) => s.get_remote_static(), - Self::RespStart(s) => s.get_remote_static(), + Self::InitiatorIkStart(s) => s.get_remote_static(), + Self::InitiatorIkSent(s) => s.get_remote_static(), + Self::InitiatorIkHsDone(s) => s.get_remote_static(), + Self::InitiatorXxStart(s) => s.get_remote_static(), + Self::InitiatorXxSent(s) => s.get_remote_static(), + Self::InitiatorXxFinalMsg(s) => s.get_remote_static(), + Self::InitiatorXxHsDone(s) => s.get_remote_static(), + Self::RespIkStart(s) => s.get_remote_static(), + Self::RespIkHsDone(s) => s.get_remote_static(), + Self::RespXxStart(s) => s.get_remote_static(), + Self::RespXxReceivedFirst(s) => s.get_remote_static(), + Self::RespXxAwaitingFinal(s) => s.get_remote_static(), + Self::RespXxHsDone(s) => s.get_remote_static(), Self::EncReady(s) => s.get_remote_static(), Self::Ready(s) => s.get_remote_static(), Self::Invalid => None, @@ -57,9 +103,19 @@ impl State { /// Get the local public key. fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { Some(match self { - State::InitiatorStart(s) => s.get_local_public_key(), - State::InitiatorSent(s) => s.get_local_public_key(), - State::RespStart(s) => s.get_local_public_key(), + State::InitiatorIkStart(s) => s.get_local_public_key(), + State::InitiatorIkSent(s) => s.get_local_public_key(), + State::InitiatorIkHsDone(s) => s.get_local_public_key(), + State::InitiatorXxStart(s) => s.get_local_public_key(), + State::InitiatorXxSent(s) => s.get_local_public_key(), + State::InitiatorXxFinalMsg(s) => s.get_local_public_key(), + State::InitiatorXxHsDone(s) => s.get_local_public_key(), + State::RespIkStart(s) => s.get_local_public_key(), + State::RespIkHsDone(s) => s.get_local_public_key(), + State::RespXxStart(s) => s.get_local_public_key(), + State::RespXxReceivedFirst(s) => s.get_local_public_key(), + State::RespXxAwaitingFinal(s) => s.get_local_public_key(), + State::RespXxHsDone(s) => s.get_local_public_key(), State::EncReady(s) => s.get_local_public_key(), State::Ready(s) => s.get_local_public_key(), State::Invalid => return None, @@ -96,9 +152,9 @@ impl SansIoCipher { plain_rx: Default::default(), } } - fn new_init(state: SecStream>) -> Self { + fn new_init(state: SecStream>) -> Self { Self { - state: State::InitiatorStart(state), + state: State::InitiatorIkStart(state), encrypted_tx: Default::default(), encrypted_rx: Default::default(), plain_tx: Default::default(), @@ -106,9 +162,29 @@ impl SansIoCipher { } } - fn new_resp(state: SecStream>) -> Self { + fn new_resp(state: SecStream>) -> Self { Self { - state: State::RespStart(state), + state: State::RespIkStart(state), + encrypted_tx: Default::default(), + encrypted_rx: Default::default(), + plain_tx: Default::default(), + plain_rx: Default::default(), + } + } + + fn new_init_xx(state: SecStream>) -> Self { + Self { + state: State::InitiatorXxStart(state), + encrypted_tx: Default::default(), + encrypted_rx: Default::default(), + plain_tx: Default::default(), + plain_rx: Default::default(), + } + } + + fn new_resp_xx(state: SecStream>) -> Self { + Self { + state: State::RespXxStart(state), encrypted_tx: Default::default(), encrypted_rx: Default::default(), plain_tx: Default::default(), @@ -119,10 +195,10 @@ impl SansIoCipher { #[instrument(skip_all, err)] fn handshake_start(&mut self, payload: &[u8]) -> Result<(), std::io::Error> { match replace(&mut self.state, State::Invalid) { - State::InitiatorStart(s) => { + State::InitiatorIkStart(s) => { let (s2, out) = s.write_msg(Some(payload))?; self.encrypted_tx.push_back(out); - self.state = State::InitiatorSent(s2); + self.state = State::InitiatorIkSent(s2); Ok(()) } _e => todo!("{_e:?}"), @@ -143,23 +219,27 @@ impl SansIoCipher { ); match replace(&mut self.state, State::Invalid) { - State::InitiatorSent(s) => { + // IK Initiator: HsMsgSent -> HsDone -> EncReady + State::InitiatorIkSent(s) => { let Some(msg) = self.encrypted_rx.pop_front() else { - self.state = State::InitiatorSent(s); + self.state = State::InitiatorIkSent(s); return Ok(None); }; let (s2, payload) = s.read_msg(&msg?)?; // Ensure payload jumps to the front of the line self.plain_rx.push_front(Event::HandshakePayload(payload)); - let (s3, out) = s2.write_msg()?; - self.encrypted_tx.push_front(out); + + // Send the setup message + let (s3, setup_msg) = s2.write_msg()?; + self.encrypted_tx.push_front(setup_msg); self.state = State::EncReady(s3); Ok(Some(())) } - State::RespStart(s) => { + // IK Responder: Start -> HsDone -> EncReady + State::RespIkStart(s) => { let Some(msg) = self.encrypted_rx.pop_front() else { // Not ready - self.state = State::RespStart(s); + self.state = State::RespIkStart(s); return Ok(None); }; let (s2, payload) = s.read_msg(&msg?)?; @@ -172,6 +252,88 @@ impl SansIoCipher { self.state = State::EncReady(s3); Ok(Some(())) } + // IK Initiator: Start -> HsMsgSent + State::InitiatorIkStart(s) => { + // no handshake message.. We use first thing in plain_tx, but maybe it should be an + // error bc we might want the payload to be handled explicitly + let payload = self.plain_tx.pop_front(); + let (s2, out) = s.write_msg(payload.as_deref())?; + self.encrypted_tx.push_back(out); + self.state = State::InitiatorIkSent(s2); + Ok(Some(())) + } + + // XX Initiator: Start -> HsMsgSent + State::InitiatorXxStart(s) => { + let payload = self.plain_tx.pop_front(); + let (s2, out) = s.write_msg(payload.as_deref())?; + self.encrypted_tx.push_back(out); + self.state = State::InitiatorXxSent(s2); + Ok(Some(())) + } + // XX Responder: Start -> RespXxReceivedFirst -> ResponderXxAwaitingFinal + State::RespXxStart(s) => { + let Some(msg) = self.encrypted_rx.pop_front() else { + self.state = State::RespXxStart(s); + return Ok(None); + }; + let (s2, payload) = s.read_msg(&msg?)?; + // Ensure payload jumps to the front of the line + self.plain_rx.push_front(Event::HandshakePayload(payload)); + let next_tx = self.plain_tx.pop_front(); + let (s3, [msg1, _should_be_empty]) = s2.write_msg(next_tx.as_deref())?; + debug_assert!(_should_be_empty.is_empty()); + self.encrypted_tx.push_front(msg1); + + self.state = State::RespXxAwaitingFinal(s3); + Ok(Some(())) + } + + // XX Initiator: HsMsgSent -> InitiatorXxFinalMsg -> InitiatorXxHsDone + State::InitiatorXxSent(s) => { + let Some(msg) = self.encrypted_rx.pop_front() else { + self.state = State::InitiatorXxSent(s); + return Ok(None); + }; + let (s2, payload) = s.read_msg(&msg?)?; + if !payload.is_empty() { + self.plain_rx.push_front(Event::HandshakePayload(payload)); + } + let (s3, msg1) = s2.write_msg()?; + let (s4, msg2) = s3.write_msg()?; + self.encrypted_tx.push_front(msg2); + self.encrypted_tx.push_front(msg1); + + self.state = State::EncReady(s4); + Ok(Some(())) + } + // XX Responder: ResponderXxAwaitingFinal -> HsDone + State::RespXxAwaitingFinal(s) => { + let Some(msg) = self.encrypted_rx.pop_front() else { + self.state = State::RespXxAwaitingFinal(s); + return Ok(None); + }; + let (s2, payload) = s.read_msg(&msg?)?; + // Third message typically has no payload, but we handle it anyway + if !payload.is_empty() { + self.plain_rx.push_front(Event::HandshakePayload(payload)); + } + //let next_tx = self.plain_tx.pop_front(); + let (s3, msg1) = s2.write_msg()?; + self.encrypted_tx.push_front(msg1); + self.state = State::EncReady(s3); + Ok(Some(())) + } + + // XX Responder: HsDone -> EncReady + State::RespXxHsDone(s) => { + // Send setup message + let (s3, setup_msg) = s.write_msg()?; + self.encrypted_tx.push_front(setup_msg); + self.state = State::EncReady(s3); + Ok(Some(())) + } + State::EncReady(mut s) => { let mut made_progress = false; while let Some(mut msg) = self.plain_tx.pop_front() { @@ -211,15 +373,12 @@ impl SansIoCipher { self.state = State::Ready(s); Ok(if made_progress { Some(()) } else { None }) } - State::InitiatorStart(s) => { - // no handshake message.. We use first thing in plain_tx, but maybe it should be an - // error bc we might want the payload to be handled explicitly - let payload = self.plain_tx.pop_front(); - let (s2, out) = s.write_msg(payload.as_deref())?; - self.encrypted_tx.push_back(out); - self.state = State::InitiatorSent(s2); - Ok(Some(())) + State::InitiatorIkHsDone(_) | State::RespIkHsDone(_) => { + todo!("Unexpected HsDone state in poll_encrypt_decrypt") } + State::RespXxReceivedFirst(_) + | State::InitiatorXxFinalMsg(_) + | State::InitiatorXxHsDone(_) => todo!(), State::Invalid => Err(IoError::other("Invalid state")), } } @@ -362,65 +521,113 @@ impl Cipher { Self { io, inner } } - /// Create a new initiator + /// Create a new initiator with the specified Noise pattern + pub fn new_dht_init_with_pattern( + io: Option>>, + pattern: HandshakePattern, + remote_pub_key: Option<&[u8; PUBLIC_KEYLEN]>, + prologue: &[u8], + ) -> Result { + let inner = match pattern { + HandshakePattern::IK => { + let remote_key = remote_pub_key.ok_or(Error::MissingRemoteKey)?; + let ss = SecStream::new_initiator_ik(remote_key, prologue)?; + SansIoCipher::new(State::InitiatorIkStart(ss)) + } + HandshakePattern::XX => { + if remote_pub_key.is_some() { + return Err(Error::UnexpectedRemoteKey); + } + let ss = SecStream::new_initiator_xx(prologue)?; + SansIoCipher::new(State::InitiatorXxStart(ss)) + } + }; + Ok(Self::new(io, inner)) + } + + /// Create a new initiator (backward compatible, uses IK pattern) pub fn new_dht_init( io: Option>>, remote_pub_key: &[u8; PUBLIC_KEYLEN], prologue: &[u8], ) -> Result { - let ss = SecStream::new_initiator(remote_pub_key, prologue)?; - let state = State::InitiatorStart(ss); - let inner = SansIoCipher::new(state); - Ok(Self::new(io, inner)) + Self::new_dht_init_with_pattern(io, HandshakePattern::IK, Some(remote_pub_key), prologue) } - /// Create a new initiator + /// Create a new initiator (IK pattern) pub fn new_init( io: Box>, - state: SecStream>, + state: SecStream>, ) -> Self { Self::new(Some(io), SansIoCipher::new_init(state)) } - /// Create a new responder from a private key + /// Create a new responder from a private key with the specified pattern + pub fn resp_from_private_with_pattern( + io: Option>>, + keypair: &Keypair, + pattern: HandshakePattern, + prologue: &[u8], + ) -> Result { + let inner = match pattern { + HandshakePattern::IK => { + let ss = SecStream::new_responder_ik(keypair, prologue)?; + SansIoCipher::new(State::RespIkStart(ss)) + } + HandshakePattern::XX => { + let ss = SecStream::new_responder_xx(keypair, prologue)?; + SansIoCipher::new(State::RespXxStart(ss)) + } + }; + Ok(Self::new(io, inner)) + } + + /// Create a new responder from a private key (backward compatible, uses IK pattern) pub fn resp_from_private( io: Option>>, keypair: &Keypair, ) -> Result { - Self::resp_from_private_with_prologue(io, keypair, &[]) + Self::resp_from_private_with_pattern(io, keypair, HandshakePattern::default(), &[]) } - /// Create a new responder from a private key with a prologue + /// Create a new responder from a private key with a prologue (backward compatible, uses IK pattern) pub fn resp_from_private_with_prologue( io: Option>>, keypair: &Keypair, prologue: &[u8], ) -> Result { - let ss = SecStream::new_responder_with_prologue(keypair, prologue)?; - let state = State::RespStart(ss); - let inner = SansIoCipher::new(state); - Ok(Self::new(io, inner)) + Self::resp_from_private_with_pattern(io, keypair, HandshakePattern::default(), prologue) } - /// Create a new responder + /// Create a new responder (IK pattern) pub fn new_resp( io: Box>, - state: SecStream>, + state: SecStream>, ) -> Self { Self::new(Some(io), SansIoCipher::new_resp(state)) } /// Wait for handshake to complete + #[cfg(test)] pub async fn complete_handshake(&mut self) -> Result<(), IoError> { use futures::{SinkExt, StreamExt}; loop { if !self.inner.ready() { - self.send(vec![]).await?; + use std::time::Duration; + + self.poll_encrypt_decrypt()?; + _ = tokio::time::timeout(Duration::from_millis(100), self.flush()).await; + if self.inner.ready() { + return Ok(()); + } + let x = tokio::time::timeout(Duration::from_millis(100), self.next()).await; if self.inner.ready() { + if let Ok(Some(event)) = x { + self.inner.plain_rx.push_front(event); + } return Ok(()); } - let _ = self.next().await; } else { return Ok(()); } @@ -777,7 +984,10 @@ mod tests { #[expect(clippy::type_complexity)] fn new_connected_secret_streams() -> ( snow::Keypair, - (SecStream>, SecStream>), + ( + SecStream>, + SecStream>, + ), ) { let kp = hc_specific::generate_keypair().unwrap(); let ssi = SecStream::new_initiator(&kp.public.clone().try_into().unwrap(), &[]).unwrap(); @@ -816,6 +1026,40 @@ mod tests { (kp, (lm, rm)) } + // XX pattern helper functions + #[expect(clippy::type_complexity)] + fn new_connected_secret_streams_xx() -> ( + snow::Keypair, + ( + SecStream>, + SecStream>, + ), + ) { + let kp = hc_specific::generate_keypair().unwrap(); + let ssi = SecStream::new_initiator_xx(&[]).unwrap(); + let ssr = SecStream::new_responder_xx(&kp, &[]).unwrap(); + (kp, (ssi, ssr)) + } + + fn connected_machines_xx() -> (snow::Keypair, (Cipher, Cipher)) { + let (kp, (_init_state, _resp_state)) = new_connected_secret_streams_xx(); + let (lio, rio) = new_connected_streams(); + + let init_cipher = + Cipher::new_dht_init_with_pattern(Some(Box::new(lio)), HandshakePattern::XX, None, &[]) + .unwrap(); + + let resp_cipher = Cipher::resp_from_private_with_pattern( + Some(Box::new(rio)), + &kp, + HandshakePattern::XX, + &[], + ) + .unwrap(); + + (kp, (init_cipher, resp_cipher)) + } + #[test] fn sans_io() -> Result<(), Error> { let (_, (lss, rss)) = new_connected_secret_streams(); @@ -957,7 +1201,7 @@ mod tests { machine.handshake_start(payload)?; // Should have transitioned to InitiatorSent state - assert!(matches!(machine.inner.state, State::InitiatorSent(_))); + assert!(matches!(machine.inner.state, State::InitiatorIkSent(_))); // Should have queued encrypted handshake message assert!(!machine.inner.encrypted_tx.is_empty()); @@ -986,7 +1230,7 @@ mod tests { let machine = Cipher::new_init(Box::new(mock_io), initiator_state); // Verify initial state - assert!(matches!(machine.inner.state, State::InitiatorStart(_))); + assert!(matches!(machine.inner.state, State::InitiatorIkStart(_))); assert!(machine.inner.plain_tx.is_empty()); assert!(machine.inner.plain_rx.is_empty()); @@ -1104,4 +1348,164 @@ mod tests { Ok(()) } + + // ===== XX Pattern Tests ===== + + #[test] + fn sans_io_xx() -> Result<(), Error> { + let (_, (init, resp)) = new_connected_secret_streams_xx(); + let (mut init, mut resp) = ( + SansIoCipher::new_init_xx(init), + SansIoCipher::new_resp_xx(resp), + ); + + // Round 1: Initiator -> Responder (ephemeral key) + let init_msg1 = init.get_sendable_messages()?; // Start -> HsMsgSent + assert_eq!(init_msg1.len(), 1); + resp.receive_next_messages(init_msg1); + + // Round 2: Responder -> Initiator (ephemeral + static key) + let resp_msg1 = resp.get_sendable_messages()?; + assert_eq!(resp_msg1.len(), 1); + init.receive_next_messages(resp_msg1); // HsMsgSent -> HsMsgSent + + // Round 3: Initiator -> Responder: two messages, static key & third handshake message + let init_msg2 = init.get_sendable_messages()?; // HsMsgSent -> EncryptorReady + assert_eq!(init_msg2.len(), 2); + resp.receive_next_messages(init_msg2); + + // Round 4: + let resp_msg2 = resp.get_sendable_messages()?; + assert_eq!(resp_msg2.len(), 1); + assert!(resp.ready()); + + // last message is enqueud but not processed + init.receive_next_messages(resp_msg2); + init.poll_encrypt_decrypt()?; + assert!(init.ready()); + + Ok(()) + } + + #[tokio::test] + async fn test_complete_handshake_xx() -> Result<(), Error> { + let (_, (mut init, mut resp)) = connected_machines_xx(); + let (init_res, resp_res) = join!(init.complete_handshake(), resp.complete_handshake()); + init_res?; + resp_res?; + assert!(init.inner.ready()); + assert!(resp.inner.ready()); + Ok(()) + } + + #[test] + fn test_get_remote_static_sans_io_xx() -> Result<(), Error> { + let (kp, (init, resp)) = new_connected_secret_streams_xx(); + let resp_pub = kp.public.try_into().unwrap(); + let (mut init, mut resp) = ( + SansIoCipher::new_init_xx(init), + SansIoCipher::new_resp_xx(resp), + ); + + // Round 1: Initiator -> Responder (ephemeral key) + let init_msg1 = init.get_sendable_messages()?; // Start -> HsMsgSent + assert_eq!(init_msg1.len(), 1); + resp.receive_next_messages(init_msg1); + + // Round 2: Responder -> Initiator (ephemeral + static key) + let resp_msg1 = resp.get_sendable_messages()?; + assert_eq!(resp_msg1.len(), 1); + init.receive_next_messages(resp_msg1); // HsMsgSent -> HsMsgSent + + // Round 3: Initiator -> Responder: two messages, static key & third handshake message + let init_msg2 = init.get_sendable_messages()?; // HsMsgSent -> EncryptorReady + assert_eq!(init_msg2.len(), 2); + resp.receive_next_messages(init_msg2); + + // Round 4: + let resp_msg2 = resp.get_sendable_messages()?; + assert_eq!(resp_msg2.len(), 1); + assert!(resp.ready()); + + // last message is enqueud but not processed + init.receive_next_messages(resp_msg2); + init.poll_encrypt_decrypt()?; + assert!(init.ready()); + + // After handshake: both sides should know each other's keys + assert_eq!(init.get_remote_static(), Some(resp_pub)); + assert!(resp.get_remote_static().is_some()); + + Ok(()) + } + + #[tokio::test] + async fn test_get_remote_static_after_handshake_xx() -> Result<(), Error> { + let (kp, (mut init, mut resp)) = connected_machines_xx(); + let resp_pub: [u8; PUBLIC_KEYLEN] = kp.public.try_into().unwrap(); + + // Before handshake: neither side has the other's key (XX pattern) + assert!(init.get_remote_static().is_none()); + assert!(resp.get_remote_static().is_none()); + + let (init_res, resp_res) = join!(init.complete_handshake(), resp.complete_handshake()); + init_res?; + resp_res?; + + // After handshake: both should know each other's keys + assert_eq!(init.get_remote_static(), Some(resp_pub)); + assert!(resp.get_remote_static().is_some()); + + Ok(()) + } + + #[tokio::test] + async fn test_handshake_hash_same_on_both_sides_xx() -> Result<(), Error> { + let (_, (mut init, mut resp)) = connected_machines_xx(); + + let (init_res, resp_res) = join!(init.complete_handshake(), resp.complete_handshake()); + init_res?; + resp_res?; + + // After handshake: both sides should have the same handshake hash + let init_hash = init.handshake_hash(); + let resp_hash = resp.handshake_hash(); + + assert!(init_hash.is_some(), "initiator should have handshake hash"); + assert!(resp_hash.is_some(), "responder should have handshake hash"); + assert_eq!( + init_hash, resp_hash, + "handshake hash should be identical on both sides" + ); + + // Hash should be 64 bytes (BLAKE2b output) + assert_eq!(init_hash.unwrap().len(), 64); + + Ok(()) + } + + #[tokio::test] + async fn test_xx_message_exchange() -> Result<(), Error> { + let (_, (mut init, mut resp)) = connected_machines_xx(); + + // Complete handshake + let (init_res, resp_res) = join!(init.complete_handshake(), resp.complete_handshake()); + init_res?; + resp_res?; + + // Test bidirectional message exchange + let msg1 = b"Hello from initiator".to_vec(); + let msg2 = b"Hello from responder".to_vec(); + + init.send(msg1.clone()).await?; + resp.send(msg2.clone()).await?; + + let recv1 = resp.next().await; + let recv2 = init.next().await; + + assert!(matches!(recv1, Some(Event::Message(m)) if m == msg1)); + assert!(matches!(recv2, Some(Event::Message(m)) if m == msg2)); + + Ok(()) + } } diff --git a/src/error.rs b/src/error.rs index 6cd4201..b649de8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,12 @@ pub enum Error { /// Error from [`std::io`] #[error("{0}")] StdIoError(#[from] std::io::Error), + /// Remote public key required for IK pattern + #[error("IK pattern requires remote public key")] + MissingRemoteKey, + /// Remote public key not expected for XX pattern + #[error("XX pattern does not use remote public key at initialization")] + UnexpectedRemoteKey, } impl From for Error { diff --git a/src/lib.rs b/src/lib.rs index 04a8dce..3e4c359 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,3 +17,4 @@ pub mod state_machine; pub use cipher::{Cipher, CipherIo, Event as CipherEvent}; pub use error::Error; +pub use state_machine::{HandshakePattern, IK, XX}; diff --git a/src/state_machine.rs b/src/state_machine.rs index b871ed1..9bd67d3 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -1,22 +1,71 @@ -//! State machine for creating a Noise IK pattern (using a typestate pattern) +//! State machine for creating a Noise IK and XX patterns (using a typestate pattern) +//! IK Pattern +//! +//! Initiator: +//! SecStream> +//! → write_msg() +//! → SecStream> +//! → read_msg() +//! → SecStream> +//! → write_msg() +//! → SecStream +//! → read_msg() +//! → SecStream +//! +//! Responder: +//! SecStream> +//! → read_msg() +//! → SecStream> +//! → write_msg() → [handshake_msg, setup_msg] +//! → SecStream +//! → read_msg() +//! → SecStream +//! +//! XX Pattern +//! +//! Initiator: +//! SecStream> +//! → write_msg() +//! → SecStream> +//! → read_msg() +//! → SecStream> +//! → write_msg() (third handshake message) +//! → SecStream> +//! → write_msg() (setup message) +//! → SecStream +//! → read_msg() +//! → SecStream +//! +//! Responder: +//! SecStream> +//! → read_msg() +//! → SecStream> +//! → write_msg() → [handshake_msg, empty_vec] +//! → SecStream> +//! → read_msg() (third handshake message) +//! → SecStream> +//! → write_msg() → setup_msg (Vec) +//! → SecStream +//! → read_msg() +//! → SecStream //! ``` //! // Excessive typing to demonstrate flow through typestates //! use hypercore_handshake::state_machine::{ -//! EncryptorReady, HsDone, HsMsgSent, Initiator, Ready, Responder, SecStream, Start, +//! EncryptorReady, HsDone, HsMsgSent, Initiator, Ready, Responder, SecStream, Start, IK, //! hc_specific::generate_keypair, //! }; //! let kp: snow::Keypair = generate_keypair()?; //! // Create an initiator and responder -//! let init: SecStream> = +//! let init: SecStream> = //! SecStream::new_initiator(&kp.public.clone().try_into().unwrap(), &[])?; -//! let resp: SecStream> = SecStream::new_responder(&kp)?; +//! let resp: SecStream> = SecStream::new_responder(&kp)?; //! //! // initiator sends the first handshake message, a payload can be included to send extra data to the //! // responder. -//! let (init, msg): (SecStream>, Vec) = init.write_msg(Some(b"one"))?; +//! let (init, msg): (SecStream>, Vec) = init.write_msg(Some(b"one"))?; //! //! // responder receives the hs message, extracts the payload -//! let (resp, payload): (SecStream>, Vec) = resp.read_msg(&msg)?; +//! let (resp, payload): (SecStream>, Vec) = resp.read_msg(&msg)?; //! assert_eq!(payload, b"one"); //! //! // responder sends a handshake message, which can include a payload. As well as a second @@ -25,7 +74,7 @@ //! resp.write_msg(Some(b"two"))?; //! //! // Initiator receives last handshake message, use handshake to create the extract payload. -//! let (init, payload_recv): (SecStream>, Vec) = init.read_msg(&msg1)?; +//! let (init, payload_recv): (SecStream>, Vec) = init.read_msg(&msg1)?; //! assert_eq!(payload_recv, b"two"); //! //! // receive decryptor keey @@ -59,9 +108,35 @@ const SNOW_CIPHERKEYLEN: usize = 32; /// Length in bytes of a public key pub const PUBLIC_KEYLEN: usize = 32; +/// Noise handshake pattern to use +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HandshakePattern { + /// IK pattern - Initiator knows responder's static public key + IK, + /// XX pattern - Mutual authentication, neither party knows the other's key beforehand + XX, +} + +impl Default for HandshakePattern { + fn default() -> Self { + // Maintain backward compatibility - IK is the default + Self::IK + } +} + +/// Pattern marker types for compile-time pattern tracking +/// IK pattern - Initiator knows responder's static public key +#[derive(Debug)] +pub struct IK; + +/// XX pattern - Mutual authentication, neither party knows the other's key beforehand +#[derive(Debug)] +pub struct XX; + /// Secret Stream protocol state pub struct SecStream { is_initiator: bool, + pattern: HandshakePattern, // Runtime pattern tracking for snow library state: HandshakeState, local_public_key: [u8; PUBLIC_KEYLEN], msg_buf: [u8; 1024], @@ -89,11 +164,17 @@ impl SecStream { self.local_public_key } + /// Get the handshake pattern being used + pub fn pattern(&self) -> HandshakePattern { + self.pattern + } + /// Get the remote peer's static public key. /// /// For Responders this is `None` until processing reading the first handshake message - /// For Initiators, this is always `Some(_)` because we use the IK which requires the Initator - /// to know the Responders public key beforehand. + /// For Initiators using IK pattern, this is always `Some(_)` because IK requires the Initiator + /// to know the Responder's public key beforehand. + /// For Initiators using XX pattern, this is `None` until the responder reveals their key. pub fn get_remote_static(&self) -> Option<[u8; PUBLIC_KEYLEN]> { self.state.get_remote_static().map(|bytes| { bytes @@ -103,37 +184,71 @@ impl SecStream { } } -/// Initiator -#[derive(Debug)] -pub struct Initiator { - _res_step: PhantomData, +/// Initiator with pattern and step tracking +pub struct Initiator { + _pattern: PhantomData, + _step: PhantomData, } -/// Initial responder state -/// This first is before it receives the first message. -/// The second is after it reads it and gets the payload, but before creating the encyptor and -/// emitting the next message. This distinction is necessary so we can handle the received payload -/// and send a new one -pub struct Responder { - _res_step: PhantomData, +impl Debug for Initiator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let pattern = std::any::type_name::() + .rsplit("::") + .next() + .unwrap_or("?"); + let step = std::any::type_name::() + .rsplit("::") + .next() + .unwrap_or("?"); + write!(f, "Initiator({})", step) + } +} + +/// Responder with pattern and step tracking +pub struct Responder { + _pattern: PhantomData, + _step: PhantomData, } -impl Debug for Responder { +impl Debug for Responder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Responder") - .field("step", &self._res_step) - .finish() + let pattern = std::any::type_name::() + .rsplit("::") + .next() + .unwrap_or("?"); + let step = std::any::type_name::() + .rsplit("::") + .next() + .unwrap_or("?"); + write!(f, "Responder({})", step) } } /// The first step. We must send or receive a handshake message to proceed. #[derive(Debug)] pub struct Start; -/// The handshake message has been sent. We must receive a handshake message to proceed to -/// [`HsDone`]. Only on [`Initiator`]. + +/// The handshake message has been sent. We must receive a handshake message to proceed. +/// Only on [`Initiator`]. #[derive(Debug)] pub struct HsMsgSent; + +/// XX-specific: Initiator has received responder's second message and must send the final handshake message. +/// Only for XX pattern on [`Initiator`]. +#[derive(Debug)] +pub struct InitiatorXxFinalMsg; + +/// XX-specific: Responder has received the first message and must send the handshake response. +/// Only for XX pattern on [`Responder`]. +#[derive(Debug)] +pub struct ResponderXxReceivedFirst; + +/// XX-specific: Responder has sent handshake response and is awaiting initiator's final message. +/// Only for XX pattern on [`Responder`]. +#[derive(Debug)] +pub struct ResponderXxAwaitingFinal; + /// [`snow::HandshakeState::is_handshake_finished`] is `true`. -/// We are ready create a [`PushStream`] and proeed to [`EncryptorReady`]. +/// We are ready to create a [`PushStream`] and proceed to [`EncryptorReady`]. #[derive(Debug)] pub struct HsDone; @@ -169,18 +284,21 @@ pub mod hc_specific { pub use snow::Keypair; use snow::{ Builder, - params::{BaseChoice, HandshakeChoice, HandshakePattern, NoiseParams}, + params::{BaseChoice, HandshakeChoice, NoiseParams}, resolvers::{DefaultResolver, FallbackResolver}, }; - /// The Hypercore specific parameter string - const PARAM_STR: &str = "Noise_IK_Ed25519_ChaChaPoly_BLAKE2b"; - static NOISE_PARAMS: LazyLock = LazyLock::new(|| { + /// The Hypercore IK parameter string + const IK_PARAM_STR: &str = "Noise_IK_Ed25519_ChaChaPoly_BLAKE2b"; + /// The Hypercore XX parameter string + const XX_PARAM_STR: &str = "Noise_XX_Ed25519_ChaChaPoly_BLAKE2b"; + + static IK_NOISE_PARAMS: LazyLock = LazyLock::new(|| { NoiseParams::new( - PARAM_STR.to_string(), + IK_PARAM_STR.to_string(), BaseChoice::Noise, HandshakeChoice { - pattern: HandshakePattern::IK, + pattern: snow::params::HandshakePattern::IK, modifiers: snow::params::HandshakeModifierList { list: vec![] }, }, snow::params::DHChoice::Curve25519, @@ -189,12 +307,30 @@ pub mod hc_specific { ) }); - /// Get Hypercore Noise parameters. - fn noise_params() -> &'static NoiseParams { - &NOISE_PARAMS + static XX_NOISE_PARAMS: LazyLock = LazyLock::new(|| { + NoiseParams::new( + XX_PARAM_STR.to_string(), + BaseChoice::Noise, + HandshakeChoice { + pattern: snow::params::HandshakePattern::XX, + modifiers: snow::params::HandshakeModifierList { list: vec![] }, + }, + snow::params::DHChoice::Curve25519, + snow::params::CipherChoice::ChaChaPoly, + snow::params::HashChoice::Blake2b, + ) + }); + + /// Get Hypercore Noise parameters for the specified pattern. + fn noise_params(pattern: crate::HandshakePattern) -> &'static NoiseParams { + match pattern { + crate::HandshakePattern::IK => &IK_NOISE_PARAMS, + crate::HandshakePattern::XX => &XX_NOISE_PARAMS, + } } - pub(super) fn builder() -> Builder<'static> { - let params = noise_params(); + + pub(super) fn builder(pattern: crate::HandshakePattern) -> Builder<'static> { + let params = noise_params(pattern); Builder::with_resolver( params.clone(), //Box::new(DefaultResolver::default()), @@ -207,19 +343,20 @@ pub mod hc_specific { /// Generate Hypercore key pair. pub fn generate_keypair() -> Result { - Ok(builder().generate_keypair()?) + // Use IK pattern for backward compatibility + Ok(builder(crate::HandshakePattern::default()).generate_keypair()?) } } -impl SecStream> { - /// Create an initiator of a secret stream - pub fn new_initiator( +impl SecStream> { + /// Create an initiator using the IK pattern (requires knowing remote's public key) + pub fn new_initiator_ik( remote_public_key: &[u8; PUBLIC_KEYLEN], prologue: &[u8], ) -> Result { let key_pair = hc_specific::generate_keypair()?; - let state = hc_specific::builder() + let state = hc_specific::builder(HandshakePattern::IK) .prologue(prologue)? .local_private_key(&key_pair.private)? .remote_public_key(remote_public_key.as_slice())? @@ -227,6 +364,7 @@ impl SecStream> { Ok(Self { is_initiator: true, + pattern: HandshakePattern::IK, state, local_public_key: key_pair .public @@ -234,20 +372,31 @@ impl SecStream> { .expect("Wrong sized key from snow?"), msg_buf: [0; 1024], step: Initiator { - _res_step: PhantomData, + _pattern: PhantomData, + _step: PhantomData, }, }) } - /// Create the first message the initiator sends to the responder + + /// Create an initiator of a secret stream (backward compatible, uses IK pattern) + pub fn new_initiator( + remote_public_key: &[u8; PUBLIC_KEYLEN], + prologue: &[u8], + ) -> Result { + Self::new_initiator_ik(remote_public_key, prologue) + } + + /// Create the first message the initiator sends to the responder (IK pattern) pub fn write_msg( mut self, payload: Option<&[u8]>, - ) -> Result<(SecStream>, Vec), Error> { + ) -> Result<(SecStream>, Vec), Error> { let payload = payload.unwrap_or_default(); let len = self.state.write_message(payload, &mut self.msg_buf)?; let msg = self.msg_buf[..len].to_vec(); let Self { is_initiator, + pattern, state, msg_buf, local_public_key, @@ -256,11 +405,13 @@ impl SecStream> { Ok(( SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, step: Initiator { - _res_step: PhantomData, + _pattern: PhantomData, + _step: PhantomData, }, }, msg, @@ -268,20 +419,75 @@ impl SecStream> { } } -impl SecStream> { - /// Create a responder of a secret stream - pub fn new_responder(keypair: &Keypair) -> Result { - Self::new_responder_with_prologue(keypair, &[]) +impl SecStream> { + /// Create an initiator using the XX pattern (anonymous handshake) + pub fn new_initiator_xx(prologue: &[u8]) -> Result { + let key_pair = hc_specific::generate_keypair()?; + + let state = hc_specific::builder(HandshakePattern::XX) + .prologue(prologue)? + .local_private_key(&key_pair.private)? + .build_initiator()?; + + Ok(Self { + is_initiator: true, + pattern: HandshakePattern::XX, + state, + local_public_key: key_pair + .public + .try_into() + .expect("Wrong sized key from snow?"), + msg_buf: [0; 1024], + step: Initiator { + _pattern: PhantomData, + _step: PhantomData, + }, + }) } - /// Create a responder of a secret stream with a prologue - pub fn new_responder_with_prologue(keypair: &Keypair, prologue: &[u8]) -> Result { - let state = hc_specific::builder() + /// Create the first message the initiator sends to the responder (XX pattern) + pub fn write_msg( + mut self, + payload: Option<&[u8]>, + ) -> Result<(SecStream>, Vec), Error> { + let payload = payload.unwrap_or_default(); + let len = self.state.write_message(payload, &mut self.msg_buf)?; + let msg = self.msg_buf[..len].to_vec(); + let Self { + is_initiator, + pattern, + state, + msg_buf, + local_public_key, + .. + } = self; + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: Initiator { + _pattern: PhantomData, + _step: PhantomData, + }, + }, + msg, + )) + } +} + +impl SecStream> { + /// Create a responder using IK pattern + pub fn new_responder_ik(keypair: &Keypair, prologue: &[u8]) -> Result { + let state = hc_specific::builder(HandshakePattern::IK) .prologue(prologue)? .local_private_key(&keypair.private)? .build_responder()?; Ok(Self { is_initiator: false, + pattern: HandshakePattern::IK, state, local_public_key: keypair .public @@ -290,20 +496,32 @@ impl SecStream> { .expect("Wrong sized key from snow?"), msg_buf: [0; 1024], step: Responder { - _res_step: PhantomData, + _pattern: PhantomData, + _step: PhantomData, }, }) } - /// Read msg and return it's payload + /// Create a responder of a secret stream (backward compatible, uses IK pattern) + pub fn new_responder(keypair: &Keypair) -> Result { + Self::new_responder_ik(keypair, &[]) + } + + /// Create a responder of a secret stream with a prologue (backward compatible, uses IK pattern) + pub fn new_responder_with_prologue(keypair: &Keypair, prologue: &[u8]) -> Result { + Self::new_responder_ik(keypair, prologue) + } + + /// Read msg and return it's payload (IK pattern) pub fn read_msg( mut self, msg: &[u8], - ) -> Result<(SecStream>, Vec), Error> { + ) -> Result<(SecStream>, Vec), Error> { let len = self.state.read_message(msg, &mut self.msg_buf)?; let payload = &self.msg_buf[..len]; let Self { is_initiator, + pattern, state, msg_buf, local_public_key, @@ -312,16 +530,19 @@ impl SecStream> { Ok(( SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, step: Responder { - _res_step: PhantomData, + _pattern: PhantomData, + _step: PhantomData, }, }, payload.to_vec(), )) } + /// Read the first message of the protocol, create the next two messages to send to the initiator. pub fn read_and_write_msg( self, @@ -332,7 +553,152 @@ impl SecStream> { } } -impl SecStream> { +impl SecStream> { + /// Create a responder using XX pattern + pub fn new_responder_xx(keypair: &Keypair, prologue: &[u8]) -> Result { + let state = hc_specific::builder(HandshakePattern::XX) + .prologue(prologue)? + .local_private_key(&keypair.private)? + .build_responder()?; + Ok(Self { + is_initiator: false, + pattern: HandshakePattern::XX, + state, + local_public_key: keypair + .public + .clone() + .try_into() + .expect("Wrong sized key from snow?"), + msg_buf: [0; 1024], + step: Responder { + _pattern: PhantomData, + _step: PhantomData, + }, + }) + } + + /// Read first message (XX pattern) + pub fn read_msg( + mut self, + msg: &[u8], + ) -> Result<(SecStream>, Vec), Error> { + let len = self.state.read_message(msg, &mut self.msg_buf)?; + let payload = &self.msg_buf[..len]; + let Self { + is_initiator, + pattern, + state, + msg_buf, + local_public_key, + .. + } = self; + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: Responder { + _pattern: PhantomData, + _step: PhantomData, + }, + }, + payload.to_vec(), + )) + } +} + +impl SecStream> { + /// Write handshake response (XX pattern) - returns [handshake_msg, empty_vec] + pub fn write_msg( + mut self, + payload: Option<&[u8]>, + ) -> Result< + ( + SecStream>, + [Vec; 2], + ), + Error, + > { + let payload = payload.unwrap_or_default(); + let len = self.state.write_message(payload, &mut self.msg_buf)?; + let hs_msg = self.msg_buf[..len].to_vec(); + + // Handshake is NOT finished yet - awaiting initiator's third message + assert!( + !self.state.is_handshake_finished(), + "XX handshake should not be finished yet" + ); + + let Self { + is_initiator, + pattern, + state, + msg_buf, + local_public_key, + .. + } = self; + + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: Responder { + _pattern: PhantomData, + _step: PhantomData, + }, + }, + [hs_msg, Vec::new()], // Second vec is empty - setup message comes later + )) + } +} + +impl SecStream> { + /// Read third handshake message from initiator (XX pattern) + pub fn read_msg( + mut self, + msg: &[u8], + ) -> Result<(SecStream>, Vec), Error> { + let len = self.state.read_message(msg, &mut self.msg_buf)?; + let payload = &self.msg_buf[..len]; + + // NOW the handshake should be finished + assert!( + self.state.is_handshake_finished(), + "XX handshake should be finished after third message" + ); + + let Self { + is_initiator, + pattern, + state, + msg_buf, + local_public_key, + .. + } = self; + + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: Responder { + _pattern: PhantomData, + _step: PhantomData, + }, + }, + payload.to_vec(), + )) + } +} + +impl SecStream> { /// Make second message with the given payload. Returns two messages, the first completes the /// Noise handshake. The second has the shared key for the remote to set up a Decryptor. pub fn write_msg( @@ -342,33 +708,41 @@ impl SecStream> { let payload = payload.unwrap_or_default(); let len = self.state.write_message(payload, &mut self.msg_buf)?; let hs_msg = self.msg_buf[..len].to_vec(); - assert!(self.state.is_handshake_finished()); + + // For IK pattern, handshake is finished after responder sends message + assert!( + self.state.is_handshake_finished(), + "IK handshake should be finished after responder's message" + ); let handshake_hash = self.state.get_handshake_hash().to_vec(); - let mut pull_stream_msg: [u8; RAW_HEADER_MSG_LEN] = [0; RAW_HEADER_MSG_LEN]; + let mut msg: [u8; RAW_HEADER_MSG_LEN] = [0; RAW_HEADER_MSG_LEN]; // write stream id to front of pull_stream_msg write_stream_id( &handshake_hash, self.is_initiator, - &mut pull_stream_msg[..STREAM_ID_LENGTH], + &mut msg[..STREAM_ID_LENGTH], ); let (tx, rx) = self.split_handshake(); let (header, pusher) = PushStream::init(OsRng, &Key::from(tx)); // write push header to back of pull_stream_msg - pull_stream_msg[STREAM_ID_LENGTH..].copy_from_slice(header.as_ref()); + msg[STREAM_ID_LENGTH..].copy_from_slice(header.as_ref()); let Self { is_initiator, + pattern, state, msg_buf, local_public_key, .. } = self; + Ok(( SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, @@ -378,21 +752,80 @@ impl SecStream> { handshake_hash, }, }, - [hs_msg, pull_stream_msg.to_vec()], + [hs_msg, msg.to_vec()], )) } } -impl SecStream> { - /// Recieve the last message to complet the handsake +impl SecStream> { + /// Send setup message (XX pattern) - handshake is already complete + pub fn write_msg(mut self) -> Result<(SecStream, Vec), Error> { + // Handshake should already be finished + assert!( + self.state.is_handshake_finished(), + "XX handshake should be finished before sending setup" + ); + + let handshake_hash = self.state.get_handshake_hash().to_vec(); + let mut msg: [u8; RAW_HEADER_MSG_LEN] = [0; RAW_HEADER_MSG_LEN]; + // write stream id to front of msg + write_stream_id( + &handshake_hash, + self.is_initiator, + &mut msg[..STREAM_ID_LENGTH], + ); + + let (tx, rx) = self.split_handshake(); + let (header, pusher) = PushStream::init(OsRng, &Key::from(tx)); + + // write push header to back of msg + msg[STREAM_ID_LENGTH..].copy_from_slice(header.as_ref()); + + let Self { + is_initiator, + pattern, + state, + msg_buf, + local_public_key, + .. + } = self; + + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: EncryptorReady { + rx: Key::from(rx), + pusher, + handshake_hash, + }, + }, + msg.to_vec(), + )) + } +} + +impl SecStream> { + /// Receive the responder's message (IK pattern) pub fn read_msg( mut self, msg: &[u8], - ) -> Result<(SecStream>, Vec), Error> { + ) -> Result<(SecStream>, Vec), Error> { let len = self.state.read_message(msg, &mut self.msg_buf)?; let payload = &self.msg_buf[..len]; + + // For IK, handshake is finished after reading responder's message + assert!( + self.state.is_handshake_finished(), + "IK handshake should be finished" + ); + let Self { is_initiator, + pattern, state, local_public_key, msg_buf, @@ -401,19 +834,20 @@ impl SecStream> { Ok(( SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, step: Initiator { - _res_step: PhantomData, + _pattern: PhantomData, + _step: PhantomData, }, }, payload.to_vec(), )) } - /// read in a message, and write the next message. Any payload in the recieved message is - /// dropped. + /// Read in a message, and write the next message. Any payload in the received message is dropped. pub fn read_and_write_msg( self, msg: &[u8], @@ -423,9 +857,146 @@ impl SecStream> { } } -impl SecStream> { - /// Write the final setup message +impl SecStream> { + /// Receive the responder's message (XX pattern) + pub fn read_msg( + mut self, + msg: &[u8], + ) -> Result<(SecStream>, Vec), Error> { + let len = self.state.read_message(msg, &mut self.msg_buf)?; + let payload = &self.msg_buf[..len]; + + // For XX, handshake is NOT finished yet - need to send third message + assert!( + !self.state.is_handshake_finished(), + "XX handshake should not be finished yet" + ); + + let Self { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + .. + } = self; + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: Initiator { + _pattern: PhantomData, + _step: PhantomData, + }, + }, + payload.to_vec(), + )) + } +} + +impl SecStream> { + /// Send the third handshake message (XX pattern) + pub fn write_msg(mut self) -> Result<(SecStream>, Vec), Error> { + let len = self.state.write_message(&[], &mut self.msg_buf)?; + let msg = self.msg_buf[..len].to_vec(); + + // NOW handshake should be finished + assert!( + self.state.is_handshake_finished(), + "XX handshake should be finished after third message" + ); + + let Self { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + .. + } = self; + + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: Initiator { + _pattern: PhantomData, + _step: PhantomData, + }, + }, + msg, + )) + } +} + +impl SecStream> { + /// Write the setup message (IK pattern) + pub fn write_msg(mut self) -> Result<(SecStream, Vec), Error> { + // Handshake must be finished + assert!( + self.state.is_handshake_finished(), + "Handshake must be finished before sending setup message" + ); + + let (tx, rx) = self.split_handshake(); + let key: [u8; SNOW_CIPHERKEYLEN] = tx[..SNOW_CIPHERKEYLEN] + .try_into() + .expect("split_tx with incorrect length"); + let key = Key::from(key); + let handshake_hash = self.state.get_handshake_hash().to_vec(); + let (header, pusher) = PushStream::init(OsRng, &key); + + let mut msg: [u8; RAW_HEADER_MSG_LEN] = [0; RAW_HEADER_MSG_LEN]; + // write stream id to front of msg + write_stream_id( + &handshake_hash, + self.is_initiator, + &mut msg[..STREAM_ID_LENGTH], + ); + // write push header to back of msg + msg[STREAM_ID_LENGTH..].copy_from_slice(header.as_ref()); + + let SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + .. + } = self; + Ok(( + SecStream { + is_initiator, + pattern, + state, + local_public_key, + msg_buf, + step: EncryptorReady { + pusher, + rx: Key::from(rx), + handshake_hash, + }, + }, + msg.to_vec(), + )) + } +} + +impl SecStream> { + /// Write the setup message (XX pattern) pub fn write_msg(mut self) -> Result<(SecStream, Vec), Error> { + // Handshake must be finished + assert!( + self.state.is_handshake_finished(), + "Handshake must be finished before sending setup message" + ); + let (tx, rx) = self.split_handshake(); let key: [u8; SNOW_CIPHERKEYLEN] = tx[..SNOW_CIPHERKEYLEN] .try_into() @@ -446,6 +1017,7 @@ impl SecStream> { let SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, @@ -454,6 +1026,7 @@ impl SecStream> { Ok(( SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, @@ -480,6 +1053,7 @@ impl SecStream { pub fn read_msg(self, msg: &[u8]) -> Result, Error> { let Self { is_initiator, + pattern, state, local_public_key, msg_buf, @@ -505,6 +1079,7 @@ impl SecStream { let puller = PullStream::init(header.into(), &rx); Ok(SecStream { is_initiator, + pattern, state, local_public_key, msg_buf, diff --git a/tests/test_xx_pattern.rs b/tests/test_xx_pattern.rs new file mode 100644 index 0000000..a81f949 --- /dev/null +++ b/tests/test_xx_pattern.rs @@ -0,0 +1,143 @@ +//! Unit test demonstrating the XX handshake pattern flow + +use hypercore_handshake::state_machine::{SecStream, hc_specific}; + +#[test] +fn test_xx_pattern_flow() -> Result<(), Box> { + // Generate keypair for responder + let resp_kp = hc_specific::generate_keypair()?; + + // For XX pattern, initiator doesn't need to know responder's public key + let initiator = SecStream::new_initiator_xx(&[])?; + let responder = SecStream::new_responder_xx(&resp_kp, &[])?; + + println!("=== XX Pattern Handshake Flow ==="); + + // Message 1: Initiator → Responder (e) + println!("\n1. Initiator sends first message (ephemeral key only)"); + let (initiator, msg0) = initiator.write_msg(Some(b"init_payload"))?; + println!(" Message 1 length: {} bytes", msg0.len()); + + // Message 2: Responder receives and responds (e, ee, s, es) + let (responder, init_payload) = responder.read_msg(&msg0)?; + assert_eq!(init_payload, b"init_payload"); + + // Responder sends response (handshake message, setup is empty for XX at this point) + println!("\n2. Responder sends first message"); + let (responder, [msg1_hs, _msg_2_empty]) = responder.write_msg(Some(b"resp_payload"))?; + assert!(_msg_2_empty.is_empty()); + println!(" Responder sends handshake message:"); + println!(" - Handshake message: {} bytes", msg1_hs.len()); + println!(" - (Setup deferred until after third handshake message)"); + + let (initiator, resp_payload) = initiator.read_msg(&msg1_hs)?; + assert_eq!(resp_payload, b"resp_payload"); + + // For XX: Initiator needs to send third handshake message (s, se) + println!("\n3. Initiator EncReady. sends final handshake message (static key)"); + let (initiator, msg3_third) = initiator.write_msg()?; + // Responder receives third message, handshake now complete + let (responder, no_payload) = responder.read_msg(&msg3_third)?; + assert!(no_payload.is_empty()); + + println!("Both sides can encrypt, but must receive decryptor message next"); + let (responder, msg4) = responder.write_msg()?; + + // Now both sides send setup messages + println!("\n6. send setup messages"); + let (initiator, init_setup) = initiator.write_msg()?; + println!(" Initiator setup message: {} bytes", init_setup.len()); + + // Complete the handshake on both sides + println!("\n7. Both sides finalize the connection"); + let mut initiator = initiator.read_msg(&msg4)?; + let mut responder = responder.read_msg(&init_setup)?; + + // Verify handshake hashes match (proves both sides completed the same handshake) + let init_hash = initiator.handshake_hash(); + let resp_hash = responder.handshake_hash(); + assert_eq!(init_hash, resp_hash, "Handshake hashes must match"); + println!(" Handshake hash matches: {:02x?}...", &init_hash[..8]); + + // Verify both sides can encrypt/decrypt messages + println!("\n7. Test encryption/decryption"); + let mut msg = b"Hello from initiator!".to_vec(); + initiator.push(&mut msg, &[], crypto_secretstream::Tag::Message)?; + println!(" Encrypted message length: {} bytes", msg.len()); + + let tag = responder.pull(&mut msg, &[])?; + assert_eq!(msg, b"Hello from initiator!"); + assert_eq!(tag, crypto_secretstream::Tag::Message); + println!(" Decrypted message: {:?}", String::from_utf8_lossy(&msg)); + + println!("\n=== XX Pattern Handshake Complete! ==="); + Ok(()) +} + +#[test] +fn test_comparison_ik_vs_xx() -> Result<(), Box> { + println!("\n=== Comparing IK vs XX Patterns ===\n"); + + let _init_kp = hc_specific::generate_keypair()?; + let resp_kp = hc_specific::generate_keypair()?; + + // IK Pattern + println!("IK Pattern:"); + let resp_pubkey: [u8; 32] = resp_kp.public.clone().try_into().unwrap(); + let ik_init = SecStream::new_initiator_ik(&resp_pubkey, &[])?; + let ik_resp = SecStream::new_responder_ik(&resp_kp, &[])?; + + let (ik_init, msg1) = ik_init.write_msg(None)?; + println!( + " - Initiator msg 1: {} bytes (includes static key)", + msg1.len() + ); + + let (_ik_resp, _) = ik_resp.read_msg(&msg1)?; + let (_ik_resp, [msg2_hs, msg2_setup]) = _ik_resp.write_msg(None)?; + println!( + " - Responder msg 2: {} bytes (handshake) + {} bytes (setup)", + msg2_hs.len(), + msg2_setup.len() + ); + + let (ik_init, _) = ik_init.read_msg(&msg2_hs)?; + let (_ik_init, msg3) = ik_init.write_msg()?; + println!(" - Initiator msg 3: {} bytes (setup only)", msg3.len()); + println!(" - Total: 3 handshake messages\n"); + + // XX Pattern + println!("XX Pattern:"); + let xx_init = SecStream::new_initiator_xx(&[])?; + let xx_resp = SecStream::new_responder_xx(&resp_kp, &[])?; + + let (xx_init, msg1) = xx_init.write_msg(None)?; + println!(" - Initiator msg 1: {} bytes (ephemeral only)", msg1.len()); + + let (xx_resp, _) = xx_resp.read_msg(&msg1)?; + let (xx_resp, [msg2_hs, _msg2_empty]) = xx_resp.write_msg(None)?; + println!( + " - Responder msg 2: {} bytes (handshake, setup deferred)", + msg2_hs.len() + ); + + let (xx_init, _) = xx_init.read_msg(&msg2_hs)?; + let (xx_init, msg3_third) = xx_init.write_msg()?; + println!( + " - Initiator msg 3: {} bytes (static key)", + msg3_third.len() + ); + + let (xx_resp, _) = xx_resp.read_msg(&msg3_third)?; + let (_xx_init, init_setup) = xx_init.write_msg()?; + let (_xx_resp, resp_setup) = xx_resp.write_msg()?; + println!(" - Initiator msg 4: {} bytes (setup)", init_setup.len()); + println!(" - Responder msg 4: {} bytes (setup)", resp_setup.len()); + println!(" - Total: 4 handshake messages (3 noise + 2 setup)\n"); + + println!("Key Difference:"); + println!(" - IK: Initiator knows responder's key upfront, sends it in first message"); + println!(" - XX: Neither knows the other's key, exchange them during handshake"); + + Ok(()) +} From a1aea2e1759dad6a91c555f55028360c9f37da70 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Fri, 13 Feb 2026 17:52:35 -0500 Subject: [PATCH 04/16] Rm redundant Cipher states --- src/cipher.rs | 43 ++----------------------------------------- src/state_machine.rs | 4 ++-- 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index 860b0ba..48fee44 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -16,8 +16,8 @@ use tracing::{instrument, trace, warn}; use crate::{ Error, HandshakePattern, IK, XX, state_machine::{ - EncryptorReady, HsDone, HsMsgSent, Initiator, InitiatorXxFinalMsg, PUBLIC_KEYLEN, Ready, - Responder, ResponderXxAwaitingFinal, ResponderXxReceivedFirst, SecStream, Start, + EncryptorReady, HsMsgSent, Initiator, PUBLIC_KEYLEN, Ready, Responder, + ResponderXxAwaitingFinal, SecStream, Start, }, }; @@ -25,23 +25,17 @@ pub(crate) enum State { // IK Initiator states InitiatorIkStart(SecStream>), InitiatorIkSent(SecStream>), - InitiatorIkHsDone(SecStream>), // IK Responder states RespIkStart(SecStream>), - RespIkHsDone(SecStream>), // XX Initiator states InitiatorXxStart(SecStream>), InitiatorXxSent(SecStream>), - InitiatorXxFinalMsg(SecStream>), - InitiatorXxHsDone(SecStream>), // XX Responder states RespXxStart(SecStream>), - RespXxReceivedFirst(SecStream>), RespXxAwaitingFinal(SecStream>), - RespXxHsDone(SecStream>), // Common states (pattern-agnostic) EncReady(SecStream), @@ -55,21 +49,15 @@ impl Debug for State { // IK pattern - Initiator Self::InitiatorIkStart(s) => f.debug_tuple("InitiatorIkStart").field(s).finish(), Self::InitiatorIkSent(s) => f.debug_tuple("InitiatorIkSent").field(s).finish(), - Self::InitiatorIkHsDone(s) => f.debug_tuple("InitiatorIkHsDone").field(s).finish(), // IK pattern - Responder Self::RespIkStart(s) => f.debug_tuple("RespIkStart").field(s).finish(), - Self::RespIkHsDone(s) => f.debug_tuple("RespIkHsDone").field(s).finish(), // XX pattern - Initiator Self::InitiatorXxStart(s) => f.debug_tuple("InitiatorXxStart").field(s).finish(), Self::InitiatorXxSent(s) => f.debug_tuple("InitiatorXxSent").field(s).finish(), - Self::InitiatorXxFinalMsg(s) => f.debug_tuple("InitiatorXxFinalMsg").field(s).finish(), - Self::InitiatorXxHsDone(s) => f.debug_tuple("InitiatorXxHsDone").field(s).finish(), // XX pattern - Responder Self::RespXxStart(s) => f.debug_tuple("RespXxStart").field(s).finish(), - Self::RespXxReceivedFirst(s) => f.debug_tuple("RespXxReceivedFirst").field(s).finish(), Self::RespXxAwaitingFinal(s) => f.debug_tuple("RespXxAwaitingFinal").field(s).finish(), - Self::RespXxHsDone(s) => f.debug_tuple("RespXxHsDone").field(s).finish(), Self::EncReady(s) => f.debug_tuple("EncReady").field(s).finish(), Self::Ready(s) => f.debug_tuple("Ready").field(s).finish(), // Bad @@ -84,17 +72,11 @@ impl State { match self { Self::InitiatorIkStart(s) => s.get_remote_static(), Self::InitiatorIkSent(s) => s.get_remote_static(), - Self::InitiatorIkHsDone(s) => s.get_remote_static(), Self::InitiatorXxStart(s) => s.get_remote_static(), Self::InitiatorXxSent(s) => s.get_remote_static(), - Self::InitiatorXxFinalMsg(s) => s.get_remote_static(), - Self::InitiatorXxHsDone(s) => s.get_remote_static(), Self::RespIkStart(s) => s.get_remote_static(), - Self::RespIkHsDone(s) => s.get_remote_static(), Self::RespXxStart(s) => s.get_remote_static(), - Self::RespXxReceivedFirst(s) => s.get_remote_static(), Self::RespXxAwaitingFinal(s) => s.get_remote_static(), - Self::RespXxHsDone(s) => s.get_remote_static(), Self::EncReady(s) => s.get_remote_static(), Self::Ready(s) => s.get_remote_static(), Self::Invalid => None, @@ -105,17 +87,11 @@ impl State { Some(match self { State::InitiatorIkStart(s) => s.get_local_public_key(), State::InitiatorIkSent(s) => s.get_local_public_key(), - State::InitiatorIkHsDone(s) => s.get_local_public_key(), State::InitiatorXxStart(s) => s.get_local_public_key(), State::InitiatorXxSent(s) => s.get_local_public_key(), - State::InitiatorXxFinalMsg(s) => s.get_local_public_key(), - State::InitiatorXxHsDone(s) => s.get_local_public_key(), State::RespIkStart(s) => s.get_local_public_key(), - State::RespIkHsDone(s) => s.get_local_public_key(), State::RespXxStart(s) => s.get_local_public_key(), - State::RespXxReceivedFirst(s) => s.get_local_public_key(), State::RespXxAwaitingFinal(s) => s.get_local_public_key(), - State::RespXxHsDone(s) => s.get_local_public_key(), State::EncReady(s) => s.get_local_public_key(), State::Ready(s) => s.get_local_public_key(), State::Invalid => return None, @@ -325,15 +301,6 @@ impl SansIoCipher { Ok(Some(())) } - // XX Responder: HsDone -> EncReady - State::RespXxHsDone(s) => { - // Send setup message - let (s3, setup_msg) = s.write_msg()?; - self.encrypted_tx.push_front(setup_msg); - self.state = State::EncReady(s3); - Ok(Some(())) - } - State::EncReady(mut s) => { let mut made_progress = false; while let Some(mut msg) = self.plain_tx.pop_front() { @@ -373,12 +340,6 @@ impl SansIoCipher { self.state = State::Ready(s); Ok(if made_progress { Some(()) } else { None }) } - State::InitiatorIkHsDone(_) | State::RespIkHsDone(_) => { - todo!("Unexpected HsDone state in poll_encrypt_decrypt") - } - State::RespXxReceivedFirst(_) - | State::InitiatorXxFinalMsg(_) - | State::InitiatorXxHsDone(_) => todo!(), State::Invalid => Err(IoError::other("Invalid state")), } } diff --git a/src/state_machine.rs b/src/state_machine.rs index 9bd67d3..dc679a7 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -200,7 +200,7 @@ impl Debug for Initiator { .rsplit("::") .next() .unwrap_or("?"); - write!(f, "Initiator({})", step) + write!(f, "Initiator[{pattern}]({})", step) } } @@ -220,7 +220,7 @@ impl Debug for Responder { .rsplit("::") .next() .unwrap_or("?"); - write!(f, "Responder({})", step) + write!(f, "Responder[{pattern}]({})", step) } } /// The first step. We must send or receive a handshake message to proceed. From faa97c027e24786b58b97a615bbb2b7326553811 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Fri, 13 Feb 2026 17:56:04 -0500 Subject: [PATCH 05/16] docs --- src/state_machine.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/state_machine.rs b/src/state_machine.rs index dc679a7..a8242c8 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -1,4 +1,10 @@ //! State machine for creating a Noise IK and XX patterns (using a typestate pattern) +//! +//! I originally chose to use a typestates here when there was just one pattern, because it made +//! state transitions obvious and brought the flow of the protocol into the typesystem. However, it +//! is **a lot** of code. +//! +//! //! IK Pattern //! //! Initiator: @@ -109,21 +115,15 @@ const SNOW_CIPHERKEYLEN: usize = 32; pub const PUBLIC_KEYLEN: usize = 32; /// Noise handshake pattern to use -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum HandshakePattern { /// IK pattern - Initiator knows responder's static public key + #[default] IK, /// XX pattern - Mutual authentication, neither party knows the other's key beforehand XX, } -impl Default for HandshakePattern { - fn default() -> Self { - // Maintain backward compatibility - IK is the default - Self::IK - } -} - /// Pattern marker types for compile-time pattern tracking /// IK pattern - Initiator knows responder's static public key #[derive(Debug)] From ba5faf93b2b9cd5b689951dede2d816901ddbfe7 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Sat, 14 Feb 2026 15:43:14 -0500 Subject: [PATCH 06/16] less code is more good --- src/cipher.rs | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index 48fee44..24515f4 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -10,7 +10,6 @@ use std::{ use crypto_secretstream::Tag; use futures::{Sink, Stream}; -use snow::Keypair; use tracing::{instrument, trace, warn}; use crate::{ @@ -129,43 +128,19 @@ impl SansIoCipher { } } fn new_init(state: SecStream>) -> Self { - Self { - state: State::InitiatorIkStart(state), - encrypted_tx: Default::default(), - encrypted_rx: Default::default(), - plain_tx: Default::default(), - plain_rx: Default::default(), - } + Self::new(State::InitiatorIkStart(state)) } fn new_resp(state: SecStream>) -> Self { - Self { - state: State::RespIkStart(state), - encrypted_tx: Default::default(), - encrypted_rx: Default::default(), - plain_tx: Default::default(), - plain_rx: Default::default(), - } + Self::new(State::RespIkStart(state)) } fn new_init_xx(state: SecStream>) -> Self { - Self { - state: State::InitiatorXxStart(state), - encrypted_tx: Default::default(), - encrypted_rx: Default::default(), - plain_tx: Default::default(), - plain_rx: Default::default(), - } + Self::new(State::InitiatorXxStart(state)) } fn new_resp_xx(state: SecStream>) -> Self { - Self { - state: State::RespXxStart(state), - encrypted_tx: Default::default(), - encrypted_rx: Default::default(), - plain_tx: Default::default(), - plain_rx: Default::default(), - } + Self::new(State::RespXxStart(state)) } #[instrument(skip_all, err)] @@ -526,7 +501,7 @@ impl Cipher { /// Create a new responder from a private key with the specified pattern pub fn resp_from_private_with_pattern( io: Option>>, - keypair: &Keypair, + keypair: &snow::Keypair, pattern: HandshakePattern, prologue: &[u8], ) -> Result { @@ -546,7 +521,7 @@ impl Cipher { /// Create a new responder from a private key (backward compatible, uses IK pattern) pub fn resp_from_private( io: Option>>, - keypair: &Keypair, + keypair: &snow::Keypair, ) -> Result { Self::resp_from_private_with_pattern(io, keypair, HandshakePattern::default(), &[]) } @@ -554,7 +529,7 @@ impl Cipher { /// Create a new responder from a private key with a prologue (backward compatible, uses IK pattern) pub fn resp_from_private_with_prologue( io: Option>>, - keypair: &Keypair, + keypair: &snow::Keypair, prologue: &[u8], ) -> Result { Self::resp_from_private_with_pattern(io, keypair, HandshakePattern::default(), prologue) From 447379b947e33134366193ab622353e984d0df62 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Sat, 14 Feb 2026 15:43:43 -0500 Subject: [PATCH 07/16] Add a way to make snow keypair --- src/crypto.rs | 10 ++++++++++ src/lib.rs | 1 + 2 files changed, 11 insertions(+) diff --git a/src/crypto.rs b/src/crypto.rs index 56e8a82..bb4e49d 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -13,6 +13,16 @@ use snow::{ }; use std::convert::TryInto; +/// Create a [`snow::Keypair`] from secret and public key bytes. +/// Note: `snow::Keypair` just holds `Vec`s. So we don't check the size. But giving it the +/// wrong size is bad. +pub fn snow_keypair_from_secret_and_public(secret: [u8; 32], public: [u8; 32]) -> snow::Keypair { + snow::Keypair { + private: secret.to_vec(), + public: public.to_vec(), + } +} + // NB: These values come from Javascript-side // // const [NS_INITIATOR, NS_RESPONDER] = crypto.namespace('hyperswarm/secret-stream', 2) diff --git a/src/lib.rs b/src/lib.rs index 3e4c359..bac5ae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,5 +16,6 @@ mod error; pub mod state_machine; pub use cipher::{Cipher, CipherIo, Event as CipherEvent}; +pub use crypto::snow_keypair_from_secret_and_public; pub use error::Error; pub use state_machine::{HandshakePattern, IK, XX}; From 8d4b300b302fbf6d631d1bed94b9ef6d6c9a885a Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Sat, 14 Feb 2026 16:48:41 -0500 Subject: [PATCH 08/16] simpler creation --- src/cipher.rs | 54 +++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index 24515f4..fa2ccb4 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -42,6 +42,26 @@ pub(crate) enum State { Invalid, } +macro_rules! state_from_ss { + ($variant:ident, $ss:ty) => { + impl From<$ss> for State { + fn from(value: $ss) -> Self { + State::$variant(value) + } + } + impl From<$ss> for SansIoCipher { + fn from(value: $ss) -> Self { + SansIoCipher::new(value.into()) + } + } + }; +} + +state_from_ss!(InitiatorIkStart, SecStream>); +state_from_ss!(RespIkStart, SecStream>); +state_from_ss!(InitiatorXxStart, SecStream>); +state_from_ss!(RespXxStart, SecStream>); + impl Debug for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -127,21 +147,6 @@ impl SansIoCipher { plain_rx: Default::default(), } } - fn new_init(state: SecStream>) -> Self { - Self::new(State::InitiatorIkStart(state)) - } - - fn new_resp(state: SecStream>) -> Self { - Self::new(State::RespIkStart(state)) - } - - fn new_init_xx(state: SecStream>) -> Self { - Self::new(State::InitiatorXxStart(state)) - } - - fn new_resp_xx(state: SecStream>) -> Self { - Self::new(State::RespXxStart(state)) - } #[instrument(skip_all, err)] fn handshake_start(&mut self, payload: &[u8]) -> Result<(), std::io::Error> { @@ -495,7 +500,7 @@ impl Cipher { io: Box>, state: SecStream>, ) -> Self { - Self::new(Some(io), SansIoCipher::new_init(state)) + Self::new(Some(io), state.into()) } /// Create a new responder from a private key with the specified pattern @@ -540,7 +545,7 @@ impl Cipher { io: Box>, state: SecStream>, ) -> Self { - Self::new(Some(io), SansIoCipher::new_resp(state)) + Self::new(Some(io), state.into()) } /// Wait for handshake to complete @@ -999,7 +1004,7 @@ mod tests { #[test] fn sans_io() -> Result<(), Error> { let (_, (lss, rss)) = new_connected_secret_streams(); - let (mut l, mut r) = (SansIoCipher::new_init(lss), SansIoCipher::new_resp(rss)); + let (mut l, mut r) = (SansIoCipher::new(lss.into()), SansIoCipher::new(rss.into())); let lx = l.get_sendable_messages()?; r.receive_next_messages(lx); @@ -1196,7 +1201,10 @@ mod tests { fn test_get_remote_static_sans_io() -> Result<(), Error> { let (kp, (init, resp)) = new_connected_secret_streams(); let resp_pub: [u8; PUBLIC_KEYLEN] = kp.public.try_into().unwrap(); - let (mut init, mut resp) = (SansIoCipher::new_init(init), SansIoCipher::new_resp(resp)); + let (mut init, mut resp) = ( + SansIoCipher::new(init.into()), + SansIoCipher::new(resp.into()), + ); // Responder doesn't know remote static before handshake assert!(resp.get_remote_static().is_none()); @@ -1291,8 +1299,8 @@ mod tests { fn sans_io_xx() -> Result<(), Error> { let (_, (init, resp)) = new_connected_secret_streams_xx(); let (mut init, mut resp) = ( - SansIoCipher::new_init_xx(init), - SansIoCipher::new_resp_xx(resp), + SansIoCipher::new(init.into()), + SansIoCipher::new(resp.into()), ); // Round 1: Initiator -> Responder (ephemeral key) @@ -1339,8 +1347,8 @@ mod tests { let (kp, (init, resp)) = new_connected_secret_streams_xx(); let resp_pub = kp.public.try_into().unwrap(); let (mut init, mut resp) = ( - SansIoCipher::new_init_xx(init), - SansIoCipher::new_resp_xx(resp), + SansIoCipher::new(init.into()), + SansIoCipher::new(resp.into()), ); // Round 1: Initiator -> Responder (ephemeral key) From e3fee72eb90f5efe75e332667e1b88d17477c9bd Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Sat, 14 Feb 2026 16:59:30 -0500 Subject: [PATCH 09/16] less API please --- src/cipher.rs | 12 ++++++------ src/state_machine.rs | 22 ++-------------------- tests/js_integration.rs | 4 ++-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index fa2ccb4..bd6fdca 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -931,8 +931,8 @@ mod tests { ), ) { let kp = hc_specific::generate_keypair().unwrap(); - let ssi = SecStream::new_initiator(&kp.public.clone().try_into().unwrap(), &[]).unwrap(); - let ssr = SecStream::new_responder(&kp).unwrap(); + let ssi = SecStream::new_initiator_ik(&kp.public.clone().try_into().unwrap(), &[]).unwrap(); + let ssr = SecStream::new_responder_ik(&kp, &[]).unwrap(); (kp, (ssi, ssr)) } @@ -1110,7 +1110,7 @@ mod tests { #[tokio::test] async fn test_machine_stream_returns_pending_when_no_data() -> Result<(), Error> { let remote_key = [3u8; 32]; - let initiator_state = SecStream::new_initiator(&remote_key, &[])?; + let initiator_state = SecStream::new_initiator_ik(&remote_key, &[])?; let (mock_io, _io_tx, _out_rx) = create_mock_io_pair(); let mut machine = Cipher::new_init(Box::new(mock_io), initiator_state); @@ -1132,7 +1132,7 @@ mod tests { async fn test_machine_handshake_start() -> Result<(), Error> { let kp = hc_specific::generate_keypair().unwrap(); let public = kp.public.try_into().unwrap(); - let initiator_state = SecStream::new_initiator(&public, &[])?; + let initiator_state = SecStream::new_initiator_ik(&public, &[])?; let (mock_io, _io_tx, mut out_rx) = create_mock_io_pair(); let mut machine = Cipher::new_init(Box::new(mock_io), initiator_state); @@ -1165,7 +1165,7 @@ mod tests { // For now, test that we can create a machine in different states let remote_key = [5u8; 32]; - let initiator_state = SecStream::new_initiator(&remote_key, &[])?; + let initiator_state = SecStream::new_initiator_ik(&remote_key, &[])?; let (mock_io, _io_tx, _out_rx) = create_mock_io_pair(); let machine = Cipher::new_init(Box::new(mock_io), initiator_state); @@ -1181,7 +1181,7 @@ mod tests { #[tokio::test] async fn test_machine_poll_ready_always_succeeds() -> Result<(), Error> { let remote_key = [6u8; 32]; - let initiator_state = SecStream::new_initiator(&remote_key, &[])?; + let initiator_state = SecStream::new_initiator_ik(&remote_key, &[])?; let (mock_io, _io_tx, _out_rx) = create_mock_io_pair(); let mut machine = Cipher::new_init(Box::new(mock_io), initiator_state); diff --git a/src/state_machine.rs b/src/state_machine.rs index a8242c8..13cb58b 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -63,8 +63,8 @@ //! let kp: snow::Keypair = generate_keypair()?; //! // Create an initiator and responder //! let init: SecStream> = -//! SecStream::new_initiator(&kp.public.clone().try_into().unwrap(), &[])?; -//! let resp: SecStream> = SecStream::new_responder(&kp)?; +//! SecStream::new_initiator_ik(&kp.public.clone().try_into().unwrap(), &[])?; +//! let resp: SecStream> = SecStream::new_responder_ik(&kp, &[])?; //! //! // initiator sends the first handshake message, a payload can be included to send extra data to the //! // responder. @@ -378,14 +378,6 @@ impl SecStream> { }) } - /// Create an initiator of a secret stream (backward compatible, uses IK pattern) - pub fn new_initiator( - remote_public_key: &[u8; PUBLIC_KEYLEN], - prologue: &[u8], - ) -> Result { - Self::new_initiator_ik(remote_public_key, prologue) - } - /// Create the first message the initiator sends to the responder (IK pattern) pub fn write_msg( mut self, @@ -502,16 +494,6 @@ impl SecStream> { }) } - /// Create a responder of a secret stream (backward compatible, uses IK pattern) - pub fn new_responder(keypair: &Keypair) -> Result { - Self::new_responder_ik(keypair, &[]) - } - - /// Create a responder of a secret stream with a prologue (backward compatible, uses IK pattern) - pub fn new_responder_with_prologue(keypair: &Keypair, prologue: &[u8]) -> Result { - Self::new_responder_ik(keypair, prologue) - } - /// Read msg and return it's payload (IK pattern) pub fn read_msg( mut self, diff --git a/tests/js_integration.rs b/tests/js_integration.rs index 79a8503..eec6f95 100644 --- a/tests/js_integration.rs +++ b/tests/js_integration.rs @@ -43,7 +43,7 @@ async fn setup_rust_initiator() -> Result<(Repl, Cipher)> { // Setup Cipher here let framed = Uint24LELengthPrefixedFraming::new(tcp.compat()); - let init = SecStream::new_initiator(&kp.public.try_into().unwrap(), &[])?; + let init = SecStream::new_initiator_ik(&kp.public.try_into().unwrap(), &[])?; let cipher = Cipher::new_init(Box::new(framed), init); Ok::<_, Error>(cipher) @@ -97,7 +97,7 @@ async fn setup_js_initiator() -> Result<(Repl, Cipher)> { // Setup Cipher here let framed = Uint24LELengthPrefixedFraming::new(tcp.compat()); - let resp = SecStream::new_responder(&kp)?; + let resp = SecStream::new_responder_ik(&kp, &[])?; let cipher = Cipher::new_resp(Box::new(framed), resp); Ok::<_, Error>(cipher) }; From 9ed95652b73e6abf1fde2fe215803c1f5f7b7c7e Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Sat, 14 Feb 2026 17:05:26 -0500 Subject: [PATCH 10/16] allow type complexity in state_machine.rs --- src/state_machine.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/state_machine.rs b/src/state_machine.rs index 13cb58b..c19d4a7 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -98,6 +98,10 @@ //! Ok::<(), Box>(()) //! ``` +#![expect( + clippy::type_complexity, + reason = "Using the type definitions would obscure the very types I'm trying to show" +)] use crypto_secretstream::{Header, Key, PullStream, PushStream, Tag}; use rand::rngs::OsRng; use snow::{HandshakeState, Keypair}; From 08fb2fde86033c6b3955d9fcc0fc6d51dcaa8d30 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Sun, 15 Feb 2026 23:45:07 -0500 Subject: [PATCH 11/16] Added CipheTrait --- src/cipher.rs | 30 ++++++++++++++++++++++++++++-- src/lib.rs | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index bd6fdca..c5ebf01 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -20,6 +20,18 @@ use crate::{ }, }; +/// Describe's the interface needed [`Cipher`] IO. +pub trait CipherTrait: + Stream + Sink, Error = std::io::Error> + Unpin + Send + Sync +{ + /// Get the public key of the remote peer + fn remote_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]>; + /// Get the local public key + fn local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]>; + /// Get the handshake hash + fn handshake_hash(&self) -> Option<&[u8]>; +} + pub(crate) enum State { // IK Initiator states InitiatorIkStart(SecStream>), @@ -129,7 +141,7 @@ impl State { /// A ["Sans-IO"](https://fasterthanli.me/articles/the-case-for-sans-io) implementation of all the /// logic of the [`Cipher`] -struct SansIoCipher { +pub struct SansIoCipher { state: State, encrypted_tx: VecDeque>, encrypted_rx: VecDeque, std::io::Error>>, @@ -458,7 +470,7 @@ impl Debug for Cipher { impl Cipher { /// Create a new [`Cipher`] - fn new(io: Option>>, inner: SansIoCipher) -> Self { + pub fn new(io: Option>>, inner: SansIoCipher) -> Self { Self { io, inner } } @@ -849,6 +861,20 @@ impl Sink> for Cipher { } } +impl CipherTrait for Cipher { + fn remote_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + self.get_remote_static() + } + + fn local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + self.get_local_public_key() + } + + fn handshake_hash(&self) -> Option<&[u8]> { + self.handshake_hash() + } +} + #[cfg(test)] mod tests { use crate::state_machine::hc_specific; diff --git a/src/lib.rs b/src/lib.rs index bac5ae5..d084de3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ mod crypto; mod error; pub mod state_machine; -pub use cipher::{Cipher, CipherIo, Event as CipherEvent}; +pub use cipher::{Cipher, CipherIo, CipherTrait, Event as CipherEvent}; pub use crypto::snow_keypair_from_secret_and_public; pub use error::Error; pub use state_machine::{HandshakePattern, IK, XX}; From 17170ee6ae42af5e68048d4a764fbc5f1a235621 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 17 Feb 2026 14:16:29 -0500 Subject: [PATCH 12/16] Make handshake_hash use vec instead of slice --- src/cipher.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index c5ebf01..a5be0a4 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -29,7 +29,7 @@ pub trait CipherTrait: /// Get the local public key fn local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]>; /// Get the handshake hash - fn handshake_hash(&self) -> Option<&[u8]>; + fn handshake_hash(&self) -> Option>; } pub(crate) enum State { @@ -870,8 +870,8 @@ impl CipherTrait for Cipher { self.get_local_public_key() } - fn handshake_hash(&self) -> Option<&[u8]> { - self.handshake_hash() + fn handshake_hash(&self) -> Option> { + self.handshake_hash().map(|h| h.to_vec()) } } From 1a494a785dfff66e5b05a40e071f03f2c7a825fb Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 17 Feb 2026 15:21:56 -0500 Subject: [PATCH 13/16] DRY State stuff that gets inner SecStream --- src/cipher.rs | 50 ++++++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index a5be0a4..64e3737 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -74,24 +74,36 @@ state_from_ss!(RespIkStart, SecStream>); state_from_ss!(InitiatorXxStart, SecStream>); state_from_ss!(RespXxStart, SecStream>); +// Because we're using typestates, and each SecStream is a different type, there's no easy way do something with SecStream that is the same for every type. So we do this: +macro_rules! delegate_to_state { + ($self:expr, $method:ident, $default:expr) => { + match $self { + State::InitiatorIkStart(s) => s.$method(), + State::InitiatorIkSent(s) => s.$method(), + State::InitiatorXxStart(s) => s.$method(), + State::InitiatorXxSent(s) => s.$method(), + State::RespIkStart(s) => s.$method(), + State::RespXxStart(s) => s.$method(), + State::RespXxAwaitingFinal(s) => s.$method(), + State::EncReady(s) => s.$method(), + State::Ready(s) => s.$method(), + State::Invalid => $default, + } + }; +} + impl Debug for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - // IK pattern - Initiator Self::InitiatorIkStart(s) => f.debug_tuple("InitiatorIkStart").field(s).finish(), Self::InitiatorIkSent(s) => f.debug_tuple("InitiatorIkSent").field(s).finish(), - // IK pattern - Responder Self::RespIkStart(s) => f.debug_tuple("RespIkStart").field(s).finish(), - - // XX pattern - Initiator Self::InitiatorXxStart(s) => f.debug_tuple("InitiatorXxStart").field(s).finish(), Self::InitiatorXxSent(s) => f.debug_tuple("InitiatorXxSent").field(s).finish(), - // XX pattern - Responder Self::RespXxStart(s) => f.debug_tuple("RespXxStart").field(s).finish(), Self::RespXxAwaitingFinal(s) => f.debug_tuple("RespXxAwaitingFinal").field(s).finish(), Self::EncReady(s) => f.debug_tuple("EncReady").field(s).finish(), Self::Ready(s) => f.debug_tuple("Ready").field(s).finish(), - // Bad Self::Invalid => write!(f, "Invalid"), } } @@ -100,33 +112,11 @@ impl Debug for State { impl State { /// Get the remote peer's static public key if available. fn get_remote_static(&self) -> Option<[u8; PUBLIC_KEYLEN]> { - match self { - Self::InitiatorIkStart(s) => s.get_remote_static(), - Self::InitiatorIkSent(s) => s.get_remote_static(), - Self::InitiatorXxStart(s) => s.get_remote_static(), - Self::InitiatorXxSent(s) => s.get_remote_static(), - Self::RespIkStart(s) => s.get_remote_static(), - Self::RespXxStart(s) => s.get_remote_static(), - Self::RespXxAwaitingFinal(s) => s.get_remote_static(), - Self::EncReady(s) => s.get_remote_static(), - Self::Ready(s) => s.get_remote_static(), - Self::Invalid => None, - } + delegate_to_state!(self, get_remote_static, None) } /// Get the local public key. fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { - Some(match self { - State::InitiatorIkStart(s) => s.get_local_public_key(), - State::InitiatorIkSent(s) => s.get_local_public_key(), - State::InitiatorXxStart(s) => s.get_local_public_key(), - State::InitiatorXxSent(s) => s.get_local_public_key(), - State::RespIkStart(s) => s.get_local_public_key(), - State::RespXxStart(s) => s.get_local_public_key(), - State::RespXxAwaitingFinal(s) => s.get_local_public_key(), - State::EncReady(s) => s.get_local_public_key(), - State::Ready(s) => s.get_local_public_key(), - State::Invalid => return None, - }) + Some(delegate_to_state!(self, get_local_public_key, return None)) } /// Get the handshake hash if available (only in Ready state). From 462a2881264a27a4cf3385b388848d5669590639 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 17 Feb 2026 15:51:44 -0500 Subject: [PATCH 14/16] fix deprecation --- src/cipher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index 64e3737..c2b1787 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -1169,8 +1169,8 @@ mod tests { let _result = machine.poll_outgoing_encrypted(&mut cx); // Should have sent handshake message to IO - let sent_msg = out_rx.try_next().unwrap(); - assert!(sent_msg.is_some()); + let sent_msg = out_rx.try_recv(); + assert!(sent_msg.is_ok()); Ok(()) } From 7ea60f684258e1491e0f7f5f5b5117aad8642c7b Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 17 Feb 2026 16:01:38 -0500 Subject: [PATCH 15/16] better docs --- src/cipher.rs | 6 +++--- src/state_machine.rs | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index c2b1787..89b0989 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -434,8 +434,8 @@ where /// # Usage modes /// /// **With IO** — Provide a [`CipherIo`] transport (a bidirectional `Stream`/`Sink`) and use -/// `Cipher` as a `Stream` / `Sink>`. Call [`complete_handshake`](Self::complete_handshake) -/// to drive the handshake to completion, then read/write through the stream/sink interface. +/// `Cipher` as a `Stream` / `Sink>`. When messages read/write through the +/// stream/sink interface, before the handshake is ready, they'll be sent as handshake payloads. /// /// **Without IO** — Create with `io: None` and drive the protocol manually: /// 1. Feed incoming ciphertext with [`receive_next`](Self::receive_next). @@ -697,7 +697,7 @@ impl Cipher { self.inner.get_remote_static() } - /// Get the local public key. It is only unavailable when we are in [`State::Invalid`]. + /// Get the local public key. It is only unavailable when Cipher handshake fails. pub fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { self.inner.get_local_public_key() } diff --git a/src/state_machine.rs b/src/state_machine.rs index c19d4a7..a082c3f 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -7,6 +7,7 @@ //! //! IK Pattern //! +//! ```text //! Initiator: //! SecStream> //! → write_msg() @@ -26,9 +27,11 @@ //! → SecStream //! → read_msg() //! → SecStream +//!``` //! //! XX Pattern //! +//!```text //! Initiator: //! SecStream> //! → write_msg() @@ -55,6 +58,9 @@ //! → read_msg() //! → SecStream //! ``` +//! +//! The flow for IK looks like this: +//! ``` //! // Excessive typing to demonstrate flow through typestates //! use hypercore_handshake::state_machine::{ //! EncryptorReady, HsDone, HsMsgSent, Initiator, Ready, Responder, SecStream, Start, IK, From 090747b2e9d3b984ef792b51494466fb3eb853f3 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 18 Feb 2026 00:34:17 -0500 Subject: [PATCH 16/16] Change CipherTrait: add is_initiator And remove Option from get_local_public_key --- src/cipher.rs | 36 ++++++++++++++++++++++++++++++------ src/state_machine.rs | 5 +++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/cipher.rs b/src/cipher.rs index 89b0989..c9c840f 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -27,9 +27,11 @@ pub trait CipherTrait: /// Get the public key of the remote peer fn remote_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]>; /// Get the local public key - fn local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]>; + fn local_public_key(&self) -> [u8; PUBLIC_KEYLEN]; /// Get the handshake hash fn handshake_hash(&self) -> Option>; + /// `true` if this is the initiator + fn is_initiator(&self) -> bool; } pub(crate) enum State { @@ -118,6 +120,10 @@ impl State { fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { Some(delegate_to_state!(self, get_local_public_key, return None)) } + /// Get the local public key. + fn is_initiator(&self) -> Option { + Some(delegate_to_state!(self, is_initiator, return None)) + } /// Get the handshake hash if available (only in Ready state). fn handshake_hash(&self) -> Option<&[u8]> { @@ -137,16 +143,26 @@ pub struct SansIoCipher { encrypted_rx: VecDeque, std::io::Error>>, plain_tx: VecDeque>, plain_rx: VecDeque, + local_public_key: [u8; 32], + is_initiator: bool, } impl SansIoCipher { fn new(state: State) -> Self { + let is_initiator = state + .is_initiator() + .expect("Creating Cipher with invalid state"); + let local_public_key = state + .get_local_public_key() + .expect("Creating Cipher with invalid state"); Self { state, encrypted_tx: Default::default(), encrypted_rx: Default::default(), plain_tx: Default::default(), plain_rx: Default::default(), + local_public_key, + is_initiator, } } @@ -376,9 +392,9 @@ impl SansIoCipher { self.state.get_remote_static() } - /// Get the local public key. It is only unavailable when we are in [`State::Invalid`]. - fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { - self.state.get_local_public_key() + /// Get the local public key + fn get_local_public_key(&self) -> [u8; PUBLIC_KEYLEN] { + self.local_public_key } /// Get the handshake hash if available (only after handshake completes). @@ -464,6 +480,10 @@ impl Cipher { Self { io, inner } } + fn is_initiator(&self) -> bool { + self.inner.is_initiator + } + /// Create a new initiator with the specified Noise pattern pub fn new_dht_init_with_pattern( io: Option>>, @@ -698,7 +718,7 @@ impl Cipher { } /// Get the local public key. It is only unavailable when Cipher handshake fails. - pub fn get_local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + pub fn get_local_public_key(&self) -> [u8; PUBLIC_KEYLEN] { self.inner.get_local_public_key() } @@ -856,13 +876,17 @@ impl CipherTrait for Cipher { self.get_remote_static() } - fn local_public_key(&self) -> Option<[u8; PUBLIC_KEYLEN]> { + fn local_public_key(&self) -> [u8; PUBLIC_KEYLEN] { self.get_local_public_key() } fn handshake_hash(&self) -> Option> { self.handshake_hash().map(|h| h.to_vec()) } + + fn is_initiator(&self) -> bool { + self.is_initiator() + } } #[cfg(test)] diff --git a/src/state_machine.rs b/src/state_machine.rs index a082c3f..19c1a88 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -192,6 +192,11 @@ impl SecStream { .expect("snow gave us a key with the wrong size?") }) } + + /// If this is the initiator + pub fn is_initiator(&self) -> bool { + self.is_initiator + } } /// Initiator with pattern and step tracking