darkbio-wire 0.17.1

Encrypted protocol between Ark and host
Documentation
// wire-rs: encrypted protocol between Ark and host
// Copyright 2026 Dark Bio AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! Encrypted sessions over a duplex byte stream.
//!
//! COBS framing separates packets, the handshake establishes encryption
//! contexts, and sealing protects messages. Each [`Client`] or [`Server`] owns
//! its receive context and shares its send context with active sends.
//! [`Sender`] handles allow other threads to send into that session.
//!
//! Adapters implement standard byte I/O plus the deadline setters in [`Read`]
//! and [`Write`]. Each outgoing frame has one configurable budget covering
//! partial writes and flush. A timeout ends that send or handshake without
//! closing the byte stream. Established-session reads wait for data or adapter
//! shutdown, with no session timeout. Handshakes use one configurable deadline
//! on each side, defaulting to 5 s.
//!
//! The client writes reset and hello before draining stale replies, so adapters
//! need enough available buffering to accept that output without concurrent
//! client reads. Backpressure may fail an attempt; the caller can retry with a
//! fresh reset.

use std::time::Duration;

mod client;
mod framing;
mod handshake;
mod io;
mod outbound;
mod sealing;
mod sender;
mod server;
mod stream;

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod clock_tests;

#[cfg(any(test, feature = "fuzz"))]
#[doc(hidden)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod mock;

#[cfg(any(test, feature = "bench", feature = "fuzz"))]
#[doc(hidden)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub mod testing;

pub use client::{Client, Roots, Verifier};
pub use io::{Read, Write};
pub use sender::Sender;
pub use server::{Attestation, Attester, Event, Server};
pub use stream::{Closer, Stream};

/// Default budget of 5 s for a handshake's output and peer replies.
///
/// Configure it with [`Client::set_handshake_timeout`] or
/// [`Server::set_handshake_timeout`]. Progress, stale frames and resets within
/// the attempt do not refresh it. Waiting for the writer lock and for caller
/// callbacks may extend the call, but cannot extend its I/O deadline.
pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);

/// Default budget of 5 s for encoding, writing and flushing one complete
/// transport frame.
///
/// Configure it with [`Stream::set_write_timeout`]. Progress does not refresh
/// it. The budget starts after acquiring the writer, excluding lock waits and
/// encryption. Handshake frames are also limited by the handshake deadline.
pub const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(5);

/// Maximum encoded frame size of 2 MiB, excluding its trailing delimiter.
///
/// An oversized incoming frame is a framing error that ends any active session.
/// Its remainder is discarded through its delimiter so the stream can carry a
/// fresh handshake.
pub const MAX_FRAME_SIZE: usize = 2 * 1024 * 1024;

/// Conservative soft limit for outgoing message sizes, guaranteed to fit a
/// frame after sealing and worst-case COBS overhead.
///
/// The wire's hard limit is [`MAX_FRAME_SIZE`]; received messages may exceed
/// this value if their encoded frames fit. Sending uses this conservative bound
/// to reject oversized messages before sealing, without advancing the
/// encryption sequence.
pub const MAX_MESSAGE_SIZE: usize = {
    let mut size = MAX_FRAME_SIZE;
    while darkbio_cobs::encode_buffer(size + sealing::OVERHEAD) > MAX_FRAME_SIZE {
        size -= 1;
    }
    size
};

/// Domain separator for the handshake's COSE envelopes,
/// [`ArkHello`](handshake::ArkHello) and [`HostAck`](handshake::HostAck).
///
/// [`HostHello`](handshake::HostHello) is plain CBOR. The separator binds
/// signatures and encryption to this protocol, preventing their reuse in
/// another protocol with the same key.
pub(crate) const CRYPTO_DOMAIN_WIRE: &[u8] = b"wire-v1";

/// HPKE info string for the ark-to-host encryption context of an established
/// session (message traffic after the handshake, not the handshake itself).
pub(crate) const CRYPTO_DOMAIN_WIRE_ARK_TO_HOST: &[u8] = b"wire-v1:ark-to-host";

/// HPKE info string for the host-to-ark encryption context of an established
/// session (message traffic after the handshake, not the handshake itself).
pub(crate) const CRYPTO_DOMAIN_WIRE_HOST_TO_ARK: &[u8] = b"wire-v1:host-to-ark";

/// Things that can go wrong in the wire transport.
#[derive(Debug, thiserror::Error)]
#[cfg_attr(
    all(any(test, feature = "fuzz"), not(docsrs)),
    derive(strum::IntoStaticStr) // the mocks name the variants in their transcripts
)]
pub enum Error {
    /// A message exceeds the sending bound [`MAX_MESSAGE_SIZE`].
    ///
    /// The size is the unencrypted message length. Refusal happens before
    /// sealing, leaving the session and its encryption sequence unchanged.
    #[error("wire packet too large: {0} bytes, max {MAX_MESSAGE_SIZE} bytes")]
    PacketTooLarge(usize),

    /// An encoded frame exceeds [`MAX_FRAME_SIZE`].
    ///
    /// On receive, the size counts bytes observed so far; the full frame may be
    /// larger. On send, the size is the required worst-case COBS encoding
    /// buffer. Receiving an oversized frame ends any active session; its
    /// remainder is discarded through the delimiter before another frame can be
    /// read.
    #[error("wire frame too large: {0} bytes, max {MAX_FRAME_SIZE} bytes")]
    FrameTooLarge(usize),

    /// A delimited frame is not valid COBS.
    ///
    /// It may have carried an encrypted message, so skipping it ends any active
    /// session. A reconnecting client discards malformed stale frames while
    /// waiting for its fresh reply.
    #[error("wire frame decode failed: {0}")]
    FrameDecodingFailed(darkbio_cobs::DecodeError),

    /// Writing a frame failed, possibly while setting its deadline or flushing.
    ///
    /// [`std::io::ErrorKind::TimedOut`] reports an expired output budget or a
    /// timeout the adapter returned. The adapter may already have accepted part
    /// or all of the frame. The affected
    /// session or handshake cannot continue. This error does not close the byte
    /// stream.
    #[error("wire send failed: {0}")]
    SendFailed(std::io::Error),

    /// An adapter read or read deadline configuration failed.
    ///
    /// Idle read timeouts and interrupted reads are retried internally.
    /// Configuration failures are returned immediately, as is expiry of an
    /// overall handshake deadline. The client ends its session; the server
    /// leaves its binding in place so the caller can decide whether to retry or
    /// disconnect. Neither side closes the stream because of this error.
    #[error("wire receive failed: {0}")]
    RecvFailed(std::io::Error),

    /// Reading reached EOF or a sender's transport owner was already released.
    ///
    /// Local closure also produces EOF once buffered frames have been consumed.
    /// This does not guarantee that a concurrent shutdown has finished.
    #[error("wire terminated")]
    Terminated,

    /// The session ended through a reset.
    ///
    /// The client returns it after the server's empty frame notification,
    /// without closing the stream, so the client can reconnect. The transport
    /// server reports a session's end through [`Event::Disconnected`] instead.
    /// The [`protocol`](crate::protocol) server closes its session with this
    /// error on that event, and when a replacement closes its predecessor.
    #[error("wire session reset by the peer")]
    SessionReset,

    /// The presented CWT could not be decoded as hardware or emulator claims.
    ///
    /// This checks the token's shape. The client's [`Verifier`] decides whether
    /// to trust a well-formed attestation.
    #[error("attestation is not for a hardware or emulator")]
    InvalidAttestation,

    /// A handshake message could not be constructed, decoded or authenticated,
    /// or the client's verifier rejected the attestation.
    ///
    /// No new session is established by this attempt. The error does not close
    /// the byte stream.
    #[error("wire handshake failed: {0}")]
    HandshakeFailed(String),

    /// A received packet could not be decrypted, or a send or receive has no
    /// current session.
    ///
    /// Invalid incoming packets end the session. Refusing an obsolete sender
    /// leaves any replacement session unaffected.
    #[error("wire encryption failed: {0}")]
    EncryptionFailed(String),
}