Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ curve25519-dalek = "4"
ed25519-dalek = { version = "2", features = ["rand_core"] }

[dev-dependencies]
futures-lite = "2.6.1"
rusty_nodejs_repl = { version = "0.4.0", features = ["integration_utils"] }
tokio = { version = "1.27.0", features = ["macros", "rt-multi-thread", "time" ] }
tokio-util = { version = "0.7.18", features = ["compat"]}
uint24le_framing = "0.2.0"
81 changes: 61 additions & 20 deletions src/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,14 @@ pub(crate) enum State {

impl Debug for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::InitiatorStart(_) => "InitiatorStart",
Self::InitiatorSent(_) => "InitiatorSent",
Self::RespStart(_) => "RespStart",
Self::EncReady(_) => "EncReady",
Self::Ready(_) => "Ready",
Self::Invalid => "Invalid",
}
)
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::Invalid => write!(f, "Invalid"),
}
}
}

Expand All @@ -57,6 +53,15 @@ impl State {
Self::Invalid => None,
}
}

/// Get the handshake hash if available (only in Ready state).
fn handshake_hash(&self) -> Option<&[u8]> {
match self {
Self::Ready(s) => Some(s.handshake_hash()),
Self::EncReady(s) => Some(s.handshake_hash()),
_ => None,
}
}
}

/// A ["Sans-IO"](https://fasterthanli.me/articles/the-case-for-sans-io) implementation of all the
Expand Down Expand Up @@ -256,6 +261,11 @@ impl SansIoCipher {
fn get_remote_static(&self) -> Option<[u8; PUBLIC_KEYLEN]> {
self.state.get_remote_static()
}

/// Get the handshake hash if available (only after handshake completes).
fn handshake_hash(&self) -> Option<&[u8]> {
self.state.handshake_hash()
}
}

impl Debug for SansIoCipher {
Expand Down Expand Up @@ -441,14 +451,6 @@ impl Cipher {
/// Encrypt outgoing messages, and decrypt encomming messages.
/// This also processes messages to complete the handshake.
fn poll_encrypt_decrypt(&mut self) -> Result<Option<()>, IoError> {
trace!(
state =? self.inner.state,
plain_tx = self.inner.plain_tx.len(),
plain_rx = self.inner.plain_rx.len(),
enc_tx = self.inner.encrypted_tx.len(),
enc_rx = self.inner.encrypted_rx.len(),
"poll_encrypt_decrypt before"
);
self.inner.poll_encrypt_decrypt()
}

Expand Down Expand Up @@ -527,6 +529,16 @@ impl Cipher {
pub fn get_remote_static(&self) -> Option<[u8; PUBLIC_KEYLEN]> {
self.inner.get_remote_static()
}

/// Get the handshake hash.
///
/// This is a unique identifier for this encrypted session, the same on both sides.
/// Used for capability verification in hypercore replication.
///
/// Returns `None` until the handshake is complete.
pub fn handshake_hash(&self) -> Option<&[u8]> {
self.inner.handshake_hash()
}
}

impl Stream for Cipher {
Expand Down Expand Up @@ -1041,4 +1053,33 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn test_handshake_hash_same_on_both_sides() -> Result<(), Error> {
let (_, (mut lm, mut rm)) = connected_machines();

// Before handshake: no handshake hash available
assert!(lm.handshake_hash().is_none());
assert!(rm.handshake_hash().is_none());

let (rl, rr) = join!(lm.complete_handshake(), rm.complete_handshake());
rl?;
rr?;

// After handshake: both sides should have the same handshake hash
let lm_hash = lm.handshake_hash();
let rm_hash = rm.handshake_hash();

assert!(lm_hash.is_some(), "initiator should have handshake hash");
assert!(rm_hash.is_some(), "responder should have handshake hash");
assert_eq!(
lm_hash, rm_hash,
"handshake hash should be identical on both sides"
);

// Hash should be 64 bytes (BLAKE2b output)
assert_eq!(lm_hash.unwrap().len(), 64);

Ok(())
}
}
34 changes: 22 additions & 12 deletions src/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ impl<Step: Debug> std::fmt::Debug for SecStream<Step> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecStream")
.field("is_initiator", &self.is_initiator)
.field("state", &self.state)
.field("msg_buf", &"[...]")
.field("step", &self.step)
.finish()
}
Expand Down Expand Up @@ -136,22 +134,16 @@ pub struct EncryptorReady {
pub struct Ready {
puller: PullStream,
pusher: PushStream,
handshake_hash: Vec<u8>,
}
impl Debug for EncryptorReady {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InitiatorEnc")
.field("rx", &"Key(..)")
.field("pusher", &"PushStream(..)")
.field("handshake_hash", &self.handshake_hash)
.finish()
f.debug_struct("EncryptorReady").finish()
}
}
impl Debug for Ready {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ready")
.field("pusher", &"PushStream(..)")
.field("puller", &"PullStream(..)")
.finish()
f.debug_struct("Ready").finish()
}
}
pub mod hc_specific {
Expand Down Expand Up @@ -444,6 +436,13 @@ impl SecStream<Initiator<HsDone>> {
}

impl SecStream<EncryptorReady> {
/// Get the handshake hash.
///
/// This is a unique identifier for this encrypted session, the same on both sides.
/// Used for capability verification in hypercore replication.
pub fn handshake_hash(&self) -> &[u8] {
&self.step.handshake_hash
}
/// Recieve message the last message, used to set up the decryption stream
pub fn read_msg(self, msg: &[u8]) -> Result<SecStream<Ready>, Error> {
let Self {
Expand Down Expand Up @@ -474,7 +473,11 @@ impl SecStream<EncryptorReady> {
is_initiator,
state,
msg_buf,
step: Ready { pusher, puller },
step: Ready {
pusher,
puller,
handshake_hash,
},
})
}

Expand Down Expand Up @@ -503,4 +506,11 @@ impl SecStream<Ready> {
pub fn pull(&mut self, msg: &mut Vec<u8>, associated_data: &[u8]) -> Result<Tag, Error> {
Ok(self.step.puller.pull(msg, associated_data)?)
}
/// Get the handshake hash.
///
/// This is a unique identifier for this encrypted session, the same on both sides.
/// Used for capability verification in hypercore replication.
pub fn handshake_hash(&self) -> &[u8] {
&self.step.handshake_hash
}
}
1 change: 1 addition & 0 deletions tests/common/js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
2 changes: 2 additions & 0 deletions tests/common/js/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules: package.json
yarn install
18 changes: 18 additions & 0 deletions tests/common/js/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use rusty_nodejs_repl::{
integration_utils::{git_root, run_make},
join_paths,
};

use std::{path::PathBuf, sync::LazyLock};

pub static REL_PATH_TO_NODE_MODULES: &str = "./tests/common/js/node_modules";
pub static REL_PATH_TO_JS_DIR: &str = "./tests/common/js";

pub static REQUIRE_JS: LazyLock<()> = LazyLock::new(|| {
let _ = run_make(REL_PATH_TO_JS_DIR, "node_modules").expect("Failed to setup node_modules");
});

pub fn path_to_node_modules() -> Result<PathBuf, Box<dyn std::error::Error>> {
let p = join_paths!(git_root()?, &REL_PATH_TO_NODE_MODULES);
Ok(p.into())
}
9 changes: 9 additions & 0 deletions tests/common/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "stuff",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@hyperswarm/secret-stream": "^6.9.1"
}
}
Loading
Loading