Skip to content

Redo the scheme's no-std interface #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 24, 2025
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
5 changes: 2 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ _default:

# Test feature flag matrix compatability.
@_test-features:
# Build and test with all features, no features, and some combinations.
# Build and test with all features, no features, and some combinations if required.
cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --all-features
cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --no-default-features
cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --no-default-features --features alloc

# Check code with MSRV compiler.
@_test-msrv:
Expand All @@ -70,7 +69,7 @@ _default:
# Test no standard library support.
@_test-no-std:
cargo install [email protected]
$HOME/.cargo/bin/cross build --package bip324 --target thumbv7m-none-eabi --no-default-features --features alloc
$HOME/.cargo/bin/cross build --package bip324 --target thumbv7m-none-eabi --no-default-features

# Run benchmarks.
bench:
Expand Down
3 changes: 1 addition & 2 deletions protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ default = ["std"]
futures = ["std", "dep:futures"]
# High-level wrappers using tokio traits - may affect MSRV requirements.
tokio = ["std", "dep:tokio"]
std = ["alloc", "bitcoin/std", "bitcoin_hashes/std", "chacha20-poly1305/std", "rand/std", "rand/std_rng"]
alloc = ["chacha20-poly1305/alloc"]
std = ["bitcoin/std", "bitcoin_hashes/std", "chacha20-poly1305/std", "rand/std", "rand/std_rng"]

[dependencies]
futures = { version = "0.3.30", default-features = false, optional = true, features = ["std"] }
Expand Down
5 changes: 2 additions & 3 deletions protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ The library is designed with a bare `no_std` and "Sans I/O" interface to keep it

The `futures` feature includes the high-level `AsyncProcotol` type which helps create and manage an encrypted channel.

The lower-level `Handshake` and `PacketHandler` types can be directly used by applications which require more control. The handshake performs the one-and-a-half round trip dance between the peers in order to generate secret materials. A successful handshake results in a packet handler which performs the encrypt and decrypt operations for the lifetime of the channel.
The lower-level `CipherSession` and `Handshake` types can be directly used by applications which require more control. The handshake performs the one-and-a-half round trip dance between the peers in order to generate secret materials. A successful handshake results in a packet handler which performs the encrypt and decrypt operations for the lifetime of the channel.

## Feature Flags

* `alloc` -- Expose memory allocation dependent features.
* `std` -- Includes the `alloc` memory allocation feature as well as extra standard library dependencies for I/O and random number generators.
* `std` -- Standard library dependencies for I/O, memory allocation, and random number generators.
* `futures` -- High level wrappers for asynchronous read and write runtimes using agnostic futures-rs traits.
* `tokio` -- Same wrappers as `futures`, but using the popular tokio runtime's specific traits instead of futures-rs.

Expand Down
63 changes: 43 additions & 20 deletions protocol/benches/packet_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

extern crate test;

use bip324::{Handshake, Network, PacketHandler, PacketType, Role};
use bip324::{CipherSession, Handshake, InboundCipher, Network, OutboundCipher, PacketType, Role};
use test::{black_box, Bencher};

fn create_packet_handler_pair() -> (PacketHandler, PacketHandler) {
fn create_packet_handler_pair() -> (CipherSession, CipherSession) {
// Create a proper handshake between Alice and Bob.
let mut alice_init_buffer = vec![0u8; 64];
let mut alice_handshake = Handshake::new(
Expand Down Expand Up @@ -45,11 +45,12 @@ fn create_packet_handler_pair() -> (PacketHandler, PacketHandler) {
.unwrap();

// Authenticate.
let mut packet_buffer = vec![0u8; 4096];
alice_handshake
.authenticate_garbage_and_version(&bob_init_buffer[64..])
.authenticate_garbage_and_version(&bob_init_buffer[64..], &mut packet_buffer)
.unwrap();
bob_handshake
.authenticate_garbage_and_version(&alice_response_buffer)
.authenticate_garbage_and_version(&alice_response_buffer, &mut packet_buffer)
.unwrap();

let alice = alice_handshake.finalize().unwrap();
Expand All @@ -65,20 +66,31 @@ fn bench_round_trip_small_packet(b: &mut Bencher) {

b.iter(|| {
// Encrypt the packet.
let encrypted = alice
.writer()
.encrypt_packet(black_box(plaintext), None, PacketType::Genuine)
let packet_len = OutboundCipher::encryption_buffer_len(plaintext.len());
let mut encrypted = vec![0u8; packet_len];
alice
.outbound()
.encrypt(
black_box(plaintext),
&mut encrypted,
PacketType::Genuine,
None,
)
.unwrap();

// Decrypt the length from first 3 bytes (real-world step).
let packet_length = bob
.reader()
.decypt_len(black_box(encrypted[0..3].try_into().unwrap()));
.inbound()
.decrypt_packet_len(black_box(encrypted[0..3].try_into().unwrap()));

// Decrypt the payload using the decrypted length.
let decrypted = bob
.reader()
.decrypt_payload(black_box(&encrypted[3..3 + packet_length]), None)
let mut decrypted = vec![0u8; InboundCipher::decryption_buffer_len(packet_length)];
bob.inbound()
.decrypt(
black_box(&encrypted[3..3 + packet_length]),
&mut decrypted,
None,
)
.unwrap();

// Ensure the final result isn't optimized away.
Expand All @@ -93,20 +105,31 @@ fn bench_round_trip_large_packet(b: &mut Bencher) {

b.iter(|| {
// Encrypt the packet.
let encrypted = alice
.writer()
.encrypt_packet(black_box(&plaintext), None, PacketType::Genuine)
let packet_len = OutboundCipher::encryption_buffer_len(plaintext.len());
let mut encrypted = vec![0u8; packet_len];
alice
.outbound()
.encrypt(
black_box(&plaintext),
&mut encrypted,
PacketType::Genuine,
None,
)
.unwrap();

// Decrypt the length from first 3 bytes (real-world step).
let packet_length = bob
.reader()
.decypt_len(black_box(encrypted[0..3].try_into().unwrap()));
.inbound()
.decrypt_packet_len(black_box(encrypted[0..3].try_into().unwrap()));

// Decrypt the payload using the decrypted length.
let decrypted = bob
.reader()
.decrypt_payload(black_box(&encrypted[3..3 + packet_length]), None)
let mut decrypted = vec![0u8; InboundCipher::decryption_buffer_len(packet_length)];
bob.inbound()
.decrypt(
black_box(&encrypted[3..3 + packet_length]),
&mut decrypted,
None,
)
.unwrap();

// Ensure the final result isn't optimized away.
Expand Down
5 changes: 3 additions & 2 deletions protocol/fuzz/fuzz_targets/handshake.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![no_main]
use bip324::{Handshake, Network, Role};
use bip324::{Handshake, Network, Role, NUM_INITIAL_HANDSHAKE_BUFFER_BYTES};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
Expand Down Expand Up @@ -52,6 +52,7 @@ fuzz_target!(|data: &[u8]| {
// Exercising malformed public key handling.
let _ = handshake.complete_materials(fuzzed_responder_pubkey, &mut garbage_and_version, None);
// Check how a broken handshake is handled.
let _ = handshake.authenticate_garbage_and_version(&garbage_and_version);
let mut packet_buffer = vec![0u8; NUM_INITIAL_HANDSHAKE_BUFFER_BYTES]; // Initial buffer for decoy and version packets
let _ = handshake.authenticate_garbage_and_version(&garbage_and_version, &mut packet_buffer);
let _ = handshake.finalize();
});
105 changes: 73 additions & 32 deletions protocol/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

use core::fmt;

#[cfg(feature = "alloc")]
use alloc::vec;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::vec;
#[cfg(feature = "std")]
use std::vec::Vec;

use bitcoin::Network;

Expand All @@ -20,11 +20,38 @@ use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

use crate::{
Error, Handshake, PacketReader, PacketType, PacketWriter, Payload, Role,
NUM_ELLIGATOR_SWIFT_BYTES, NUM_GARBAGE_TERMINTOR_BYTES, NUM_INITIAL_HANDSHAKE_BUFFER_BYTES,
VERSION_CONTENT,
Error, Handshake, InboundCipher, OutboundCipher, PacketType, Role, NUM_ELLIGATOR_SWIFT_BYTES,
NUM_GARBAGE_TERMINTOR_BYTES, NUM_INITIAL_HANDSHAKE_BUFFER_BYTES, VERSION_CONTENT,
};

/// A decrypted BIP324 payload with its packet type.
#[cfg(feature = "std")]
pub struct Payload {
contents: Vec<u8>,
packet_type: PacketType,
}

#[cfg(feature = "std")]
impl Payload {
/// Create a new payload.
pub fn new(contents: Vec<u8>, packet_type: PacketType) -> Self {
Self {
contents,
packet_type,
}
}

/// Access the decrypted payload contents.
pub fn contents(&self) -> &[u8] {
&self.contents
}

/// Access the packet type.
pub fn packet_type(&self) -> PacketType {
self.packet_type
}
}

/// High level error type for the protocol interface.
#[cfg(feature = "std")]
#[derive(Debug)]
Expand Down Expand Up @@ -159,11 +186,11 @@ impl AsyncProtocol {
let mut remote_ellswift_buffer = [0u8; 64];
reader.read_exact(&mut remote_ellswift_buffer).await?;

let num_version_packet_bytes = PacketWriter::required_packet_allocation(&VERSION_CONTENT);
let num_version_packet_bytes = OutboundCipher::encryption_buffer_len(VERSION_CONTENT.len());
let num_decoy_packets_bytes: usize = match decoys {
Some(decoys) => decoys
.iter()
.map(|decoy| PacketWriter::required_packet_allocation(decoy))
.map(|decoy| OutboundCipher::encryption_buffer_len(decoy.len()))
.sum(),
None => 0,
};
Expand All @@ -187,6 +214,8 @@ impl AsyncProtocol {
// Keep pulling bytes from the buffer until the garbage is flushed.
let mut remote_garbage_and_version_buffer =
Vec::with_capacity(NUM_INITIAL_HANDSHAKE_BUFFER_BYTES);
let mut packet_buffer = vec![0u8; NUM_INITIAL_HANDSHAKE_BUFFER_BYTES];

loop {
let mut temp_buffer = [0u8; NUM_INITIAL_HANDSHAKE_BUFFER_BYTES];
match reader.read(&mut temp_buffer).await {
Expand All @@ -197,12 +226,17 @@ impl AsyncProtocol {
Ok(bytes_read) => {
remote_garbage_and_version_buffer.extend_from_slice(&temp_buffer[..bytes_read]);

match handshake
.authenticate_garbage_and_version(&remote_garbage_and_version_buffer)
{
match handshake.authenticate_garbage_and_version(
&remote_garbage_and_version_buffer,
&mut packet_buffer,
) {
Ok(()) => break,
// Not enough data, continue reading.
Err(Error::CiphertextTooSmall) => continue,
Err(Error::BufferTooSmall { required_bytes }) => {
packet_buffer.resize(required_bytes, 0);
continue;
}
Err(e) => return Err(ProtocolError::Internal(e)),
}
}
Expand All @@ -216,15 +250,15 @@ impl AsyncProtocol {
}
}

let packet_handler = handshake.finalize()?;
let (packet_reader, packet_writer) = packet_handler.into_split();
let cipher_session = handshake.finalize()?;
let (inbound_cipher, outbound_cipher) = cipher_session.into_split();

Ok(Self {
reader: AsyncProtocolReader {
packet_reader,
inbound_cipher,
state: DecryptState::init_reading_length(),
},
writer: AsyncProtocolWriter { packet_writer },
writer: AsyncProtocolWriter { outbound_cipher },
})
}

Expand Down Expand Up @@ -280,7 +314,7 @@ impl DecryptState {
/// Manages an async buffer to automatically decrypt contents of received packets.
#[cfg(any(feature = "futures", feature = "tokio"))]
pub struct AsyncProtocolReader {
packet_reader: PacketReader,
inbound_cipher: InboundCipher,
state: DecryptState,
}

Expand All @@ -297,7 +331,7 @@ impl AsyncProtocolReader {
/// # Returns
///
/// A `Result` containing:
/// * `Ok(Payload)`: A decrypted payload.
/// * `Ok(Payload)`: A decrypted payload with packet type.
/// * `Err(ProtocolError)`: An error that occurred during the read or decryption.
pub async fn read_and_decrypt<R>(&mut self, buffer: &mut R) -> Result<Payload, ProtocolError>
where
Expand All @@ -314,7 +348,7 @@ impl AsyncProtocolReader {
*bytes_read += buffer.read(&mut length_bytes[*bytes_read..]).await?;
}

let packet_bytes_len = self.packet_reader.decypt_len(*length_bytes);
let packet_bytes_len = self.inbound_cipher.decrypt_packet_len(*length_bytes);
self.state = DecryptState::init_reading_payload(packet_bytes_len);
}
DecryptState::ReadingPayload {
Expand All @@ -325,24 +359,28 @@ impl AsyncProtocolReader {
*bytes_read += buffer.read(&mut packet_bytes[*bytes_read..]).await?;
}

let payload = self.packet_reader.decrypt_payload(packet_bytes, None)?;
let plaintext_len = InboundCipher::decryption_buffer_len(packet_bytes.len());
let mut plaintext_buffer = vec![0u8; plaintext_len];
let packet_type =
self.inbound_cipher
.decrypt(packet_bytes, &mut plaintext_buffer, None)?;
self.state = DecryptState::init_reading_length();
return Ok(payload);
return Ok(Payload::new(plaintext_buffer, packet_type));
}
}
}
}

/// Consume the protocol reader in exchange for the underlying packet decoder.
pub fn decoder(self) -> PacketReader {
self.packet_reader
/// Consume the protocol reader in exchange for the underlying inbound cipher.
pub fn into_cipher(self) -> InboundCipher {
self.inbound_cipher
}
}

/// Manages an async buffer to automatically encrypt and send contents in packets.
#[cfg(any(feature = "futures", feature = "tokio"))]
pub struct AsyncProtocolWriter {
packet_writer: PacketWriter,
outbound_cipher: OutboundCipher,
}

#[cfg(any(feature = "futures", feature = "tokio"))]
Expand All @@ -366,16 +404,19 @@ impl AsyncProtocolWriter {
where
W: AsyncWrite + Unpin + Send,
{
let write_bytes =
self.packet_writer
.encrypt_packet(plaintext, None, PacketType::Genuine)?;
buffer.write_all(&write_bytes[..]).await?;
let packet_len = OutboundCipher::encryption_buffer_len(plaintext.len());
let mut packet_buffer = vec![0u8; packet_len];

self.outbound_cipher
.encrypt(plaintext, &mut packet_buffer, PacketType::Genuine, None)?;

buffer.write_all(&packet_buffer).await?;
buffer.flush().await?;
Ok(())
}

/// Consume the protocol writer in exchange for the underlying packet encoder.
pub fn encoder(self) -> PacketWriter {
self.packet_writer
/// Consume the protocol writer in exchange for the underlying outbound cipher.
pub fn into_cipher(self) -> OutboundCipher {
self.outbound_cipher
}
}
Loading