diff --git a/src/cipher.rs b/src/cipher.rs index 58f29ed..c9c840f 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -13,29 +13,99 @@ use futures::{Sink, Stream}; use tracing::{instrument, trace, warn}; use crate::{ - Error, + Error, HandshakePattern, IK, XX, state_machine::{ - EncryptorReady, HsMsgSent, Initiator, PUBLIC_KEYLEN, Ready, Responder, SecStream, Start, + EncryptorReady, HsMsgSent, Initiator, PUBLIC_KEYLEN, Ready, Responder, + ResponderXxAwaitingFinal, SecStream, Start, }, }; +/// 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) -> [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 { - InitiatorStart(SecStream>), - InitiatorSent(SecStream>), - RespStart(SecStream>), + // IK Initiator states + InitiatorIkStart(SecStream>), + InitiatorIkSent(SecStream>), + + // IK Responder states + RespIkStart(SecStream>), + + // XX Initiator states + InitiatorXxStart(SecStream>), + InitiatorXxSent(SecStream>), + + // XX Responder states + RespXxStart(SecStream>), + RespXxAwaitingFinal(SecStream>), + + // Common states (pattern-agnostic) EncReady(SecStream), Ready(SecStream), 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>); + +// 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 { - 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(), + Self::InitiatorIkStart(s) => f.debug_tuple("InitiatorIkStart").field(s).finish(), + Self::InitiatorIkSent(s) => f.debug_tuple("InitiatorIkSent").field(s).finish(), + Self::RespIkStart(s) => f.debug_tuple("RespIkStart").field(s).finish(), + Self::InitiatorXxStart(s) => f.debug_tuple("InitiatorXxStart").field(s).finish(), + Self::InitiatorXxSent(s) => f.debug_tuple("InitiatorXxSent").field(s).finish(), + 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(), Self::Invalid => write!(f, "Invalid"), } } @@ -44,14 +114,15 @@ 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::InitiatorStart(s) => s.get_remote_static(), - Self::InitiatorSent(s) => s.get_remote_static(), - Self::RespStart(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(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). @@ -66,51 +137,42 @@ 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>>, 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(), - } - } - fn new_init(state: SecStream>) -> Self { - Self { - state: State::InitiatorStart(state), - encrypted_tx: Default::default(), - encrypted_rx: Default::default(), - plain_tx: Default::default(), - plain_rx: Default::default(), - } - } - - fn new_resp(state: SecStream>) -> Self { - Self { - state: State::RespStart(state), - encrypted_tx: Default::default(), - encrypted_rx: Default::default(), - plain_tx: Default::default(), - plain_rx: Default::default(), + local_public_key, + is_initiator, } } #[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:?}"), @@ -131,23 +193,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?)?; @@ -160,6 +226,79 @@ 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(())) + } + State::EncReady(mut s) => { let mut made_progress = false; while let Some(mut msg) = self.plain_tx.pop_front() { @@ -199,15 +338,6 @@ 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::Invalid => Err(IoError::other("Invalid state")), } } @@ -262,6 +392,11 @@ impl SansIoCipher { self.state.get_remote_static() } + /// 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). fn handshake_hash(&self) -> Option<&[u8]> { self.state.handshake_hash() @@ -315,8 +450,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). @@ -341,69 +476,121 @@ 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 } } - /// Create a new initiator + 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>>, + 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)) + Self::new(Some(io), state.into()) } - /// 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: &snow::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>>, - private: &[u8], + keypair: &snow::Keypair, ) -> Result { - Self::resp_from_private_with_prologue(io, private, &[]) + 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>>, - private: &[u8], + keypair: &snow::Keypair, prologue: &[u8], ) -> Result { - let ss = SecStream::new_responder_with_prologue(private, 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)) + Self::new(Some(io), state.into()) } /// 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 _ = self.next().await; + 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(()); + } } else { return Ok(()); } @@ -530,6 +717,11 @@ impl Cipher { self.inner.get_remote_static() } + /// Get the local public key. It is only unavailable when Cipher handshake fails. + pub fn get_local_public_key(&self) -> [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. @@ -679,6 +871,24 @@ 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) -> [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)] mod tests { use crate::state_machine::hc_specific; @@ -755,11 +965,14 @@ 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(); - let ssr = SecStream::new_responder(&kp.private).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)) } @@ -794,10 +1007,44 @@ 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(); - 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); @@ -903,7 +1150,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); @@ -925,7 +1172,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); @@ -935,7 +1182,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()); @@ -946,8 +1193,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(()) } @@ -958,13 +1205,13 @@ 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); // 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()); @@ -974,7 +1221,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); @@ -994,7 +1241,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()); @@ -1082,4 +1332,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.into()), + SansIoCipher::new(resp.into()), + ); + + // 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.into()), + SansIoCipher::new(resp.into()), + ); + + // 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/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/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..d084de3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,5 +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}; diff --git a/src/state_machine.rs b/src/state_machine.rs index 20f4242..19c1a88 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -1,22 +1,83 @@ -//! 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) +//! +//! 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 +//! +//! ```text +//! 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 +//! +//!```text +//! 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 +//! ``` +//! +//! 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, +//! 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> = -//! SecStream::new_initiator(&kp.public.try_into().unwrap(), &[])?; -//! let resp: SecStream> = SecStream::new_responder(&kp.private)?; +//! let init: SecStream> = +//! 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. -//! 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 +86,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 @@ -43,9 +104,13 @@ //! 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; +use snow::{HandshakeState, Keypair}; use std::{fmt::Debug, marker::PhantomData}; use tracing::error; @@ -56,12 +121,34 @@ 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; + +/// Noise handshake pattern to use +#[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, +} + +/// 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], step: Step, } @@ -82,11 +169,22 @@ 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 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 @@ -94,32 +192,78 @@ 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 -#[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 -#[derive(Debug)] -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[{pattern}]({})", step) + } +} + +/// Responder with pattern and step tracking +pub struct Responder { + _pattern: PhantomData, + _step: PhantomData, +} + +impl Debug for Responder { + 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, "Responder[{pattern}]({})", 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; @@ -155,18 +299,35 @@ 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( + IK_PARAM_STR.to_string(), + BaseChoice::Noise, + HandshakeChoice { + pattern: snow::params::HandshakePattern::IK, + modifiers: snow::params::HandshakeModifierList { list: vec![] }, + }, + snow::params::DHChoice::Curve25519, + snow::params::CipherChoice::ChaChaPoly, + snow::params::HashChoice::Blake2b, + ) + }); + + static XX_NOISE_PARAMS: LazyLock = LazyLock::new(|| { NoiseParams::new( - PARAM_STR.to_string(), + XX_PARAM_STR.to_string(), BaseChoice::Noise, HandshakeChoice { - pattern: HandshakePattern::IK, + pattern: snow::params::HandshakePattern::XX, modifiers: snow::params::HandshakeModifierList { list: vec![] }, }, snow::params::DHChoice::Curve25519, @@ -175,12 +336,16 @@ pub mod hc_specific { ) }); - /// Get Hypercore Noise parameters. - fn noise_params() -> &'static NoiseParams { - &NOISE_PARAMS + /// 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()), @@ -193,19 +358,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())? @@ -213,34 +379,46 @@ impl SecStream> { Ok(Self { is_initiator: true, + pattern: HandshakePattern::IK, state, + local_public_key: key_pair + .public + .try_into() + .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 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, .. } = self; Ok(( SecStream { is_initiator, + pattern, state, + local_public_key, msg_buf, step: Initiator { - _res_step: PhantomData, + _pattern: PhantomData, + _step: PhantomData, }, }, msg, @@ -248,53 +426,120 @@ impl SecStream> { } } -impl SecStream> { - /// Create a responder of a secret stream - pub fn new_responder(private: &[u8]) -> Result { - Self::new_responder_with_prologue(private, &[]) +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 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, + )) } +} - /// Create a responder of a secret stream with a prologue - pub fn new_responder_with_prologue(private: &[u8], prologue: &[u8]) -> Result { - let state = hc_specific::builder() +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(private)? + .local_private_key(&keypair.private)? .build_responder()?; Ok(Self { is_initiator: false, + pattern: HandshakePattern::IK, state, + local_public_key: keypair + .public + .clone() + .try_into() + .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 + /// 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, .. } = self; 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, @@ -305,7 +550,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( @@ -315,33 +705,94 @@ 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, + step: EncryptorReady { + rx: Key::from(rx), + pusher, + handshake_hash, + }, + }, + [hs_msg, msg.to_vec()], + )) + } +} + +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), @@ -349,40 +800,51 @@ impl SecStream> { handshake_hash, }, }, - [hs_msg, pull_stream_msg.to_vec()], + msg.to_vec(), )) } } -impl SecStream> { - /// Recieve the last message to complet the handsake +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, .. } = self; 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], @@ -392,9 +854,93 @@ 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() @@ -415,14 +961,71 @@ impl SecStream> { 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() + .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, @@ -447,14 +1050,16 @@ impl SecStream { pub fn read_msg(self, msg: &[u8]) -> Result, Error> { let Self { is_initiator, + pattern, + 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]; @@ -471,7 +1076,9 @@ impl SecStream { let puller = PullStream::init(header.into(), &rx); Ok(SecStream { is_initiator, + pattern, state, + local_public_key, msg_buf, step: Ready { pusher, diff --git a/tests/js_integration.rs b/tests/js_integration.rs index 1d71b4c..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.private)?; + let resp = SecStream::new_responder_ik(&kp, &[])?; let cipher = Cipher::new_resp(Box::new(framed), resp); Ok::<_, Error>(cipher) }; 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(()) +}