tachyon-i2p 0.0.1

Safe async wrapper around i2pd-sys (native I2P `.b32.i2p` eepsite support).
Documentation
//! [`I2pRouter`]: the process-wide libi2pd instance.

use crate::destination::Destination;
use crate::error::I2pError;
use std::ffi::CString;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

/// Only one libi2pd router context may exist per process (it's a process-wide global inside
/// libi2pd itself, not a per-instance object) -- this guards against a second [`I2pRouter::start`]
/// silently corrupting the first one's state.
static ROUTER_STARTED: AtomicBool = AtomicBool::new(false);

/// The signature algorithm for a destination's identity -- see [`I2pRouter::generate_keys`].
///
/// Corresponds to libi2pd's stable, protocol-level `SigningKeyType` enum
/// (`libi2pd/Identity.h`), restricted to the algorithms usable for a destination identity that
/// this vendored libi2pd actually implements. Two families from the wider I2P specification are
/// deliberately not exposed:
///
/// - **RSA** (`RSA-SHA256-2048`/`RSA-SHA384-3072`/`RSA-SHA512-4096`): per the I2P cryptography
///   spec, these are an application-layer signature type used only for `su3` file signing, not
///   for destination identities. Consistent with that, this vendored libi2pd's own
///   `GenerateSigningKeyPair` silently substitutes EdDSA if asked for one of these (with a
///   warning log) -- exposing them here would just be a footgun that doesn't do what its name
///   says.
/// - **RedDSA-SHA512-Ed25519**: per `libi2pd/Identity.h`, this type is for LeaseSet2 blinding
///   only, not for signing a destination's own identity -- using it here would be relying on
///   behavior libi2pd doesn't document as supported for this purpose.
///
/// [`Eddsa25519`](Self::Eddsa25519) (the default, and the I2P network's own default since release
/// 0.9.15) is recommended for new destinations; the ECDSA options are offered for
/// interoperability with specific peer/policy requirements, and
/// [`DsaSha1`](Self::DsaSha1) only for compatibility with pre-0.9.15 persisted keys -- there's no
/// reason to choose it for a new destination.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum SigType {
    /// DSA-SHA1. The original I2P signature type, superseded as the network default in 2015 --
    /// only offered for compatibility with old persisted keys.
    DsaSha1,
    /// ECDSA-SHA256 on the P-256 curve.
    EcdsaP256,
    /// ECDSA-SHA384 on the P-384 curve. Not widely used on the I2P network.
    EcdsaP384,
    /// ECDSA-SHA512 on the P-521 curve. The strongest classical ECDSA option here; not widely
    /// used on the I2P network.
    EcdsaP521,
    /// Ed25519 (EdDSA-SHA512). The I2P network's own default since release 0.9.15, and the
    /// recommended choice for new destinations.
    #[default]
    Eddsa25519,
}

impl SigType {
    pub(crate) const fn as_raw(self) -> std::os::raw::c_int {
        match self {
            Self::DsaSha1 => 0,
            Self::EcdsaP256 => 1,
            Self::EcdsaP384 => 2,
            Self::EcdsaP521 => 3,
            Self::Eddsa25519 => 7,
        }
    }
}

/// The encryption algorithm for a destination's identity -- see [`I2pRouter::generate_keys`].
///
/// Corresponds to libi2pd's stable, protocol-level `CryptoKeyType` enum (`libi2pd/Identity.h`).
/// [`EciesX25519`](Self::EciesX25519) (the default) is the I2P network's own current default and
/// the safest choice for reachability: every current router/client on the network understands
/// it. The `EciesMlkem*` variants add a post-quantum `ML-KEM` component alongside the same
/// X25519 exchange (hybrid, not a replacement) for long-term confidentiality against a
/// future quantum adversary -- genuinely the most secure options here, but new enough that a
/// peer running an older I2P implementation may not understand a destination using one; treat
/// them as an explicit opt-in, not a default, until PQ support is universal on the network.
/// [`ElGamal`](Self::ElGamal) is the original I2P encryption scheme, kept only for compatibility
/// with old persisted keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CryptoType {
    /// The original ElGamal encryption scheme. Only offered for compatibility with old persisted
    /// keys -- there's no reason to choose it for a new destination.
    ElGamal,
    /// ECIES on the P-256 curve with AES-256-CBC. Not widely used on the I2P network.
    EciesP256,
    /// ECIES-X25519-AEAD (ChaCha20/Poly1305). The I2P network's own current default and the
    /// safest choice for reachability -- see the type-level docs for when the ML-KEM hybrid
    /// options below might be worth the reachability trade-off instead.
    #[default]
    EciesX25519,
    /// ECIES-X25519-AEAD hybridized with ML-KEM-512 (NIST PQC security category 1).
    EciesMlkem512X25519,
    /// ECIES-X25519-AEAD hybridized with ML-KEM-768 (NIST PQC security category 3) -- a
    /// reasonable balance if you specifically want a post-quantum-hardened destination.
    EciesMlkem768X25519,
    /// ECIES-X25519-AEAD hybridized with ML-KEM-1024 (NIST PQC security category 5) -- the
    /// strongest post-quantum option here.
    EciesMlkem1024X25519,
}

impl CryptoType {
    pub(crate) const fn as_raw(self) -> std::os::raw::c_int {
        match self {
            Self::ElGamal => 0,
            Self::EciesP256 => 1,
            Self::EciesX25519 => 4,
            Self::EciesMlkem512X25519 => 5,
            Self::EciesMlkem768X25519 => 6,
            Self::EciesMlkem1024X25519 => 7,
        }
    }
}

#[derive(Debug)]
struct RouterInner {
    /// Serializes [`I2pRouter::destination_from_keys_file`]'s read-check-generate-write sequence,
    /// so two concurrent calls for the *same* path (e.g. two tasks racing to create the same
    /// first-time destination) can't each independently generate and persist a different
    /// keypair -- which without this would leave whichever `Destination` didn't win the write
    /// race holding an identity that silently doesn't match what's actually on disk. Shared
    /// across every clone of this `I2pRouter` (only one router runs per process, so this is
    /// sufficient for the common single-router case); does not protect against two *separate
    /// processes* racing to write the same keys file path.
    keys_file_lock: tokio::sync::Mutex<()>,
}

impl Drop for RouterInner {
    fn drop(&mut self) {
        // SAFETY: `i2pd_stop`/`i2pd_terminate` are libi2pd's documented shutdown sequence,
        // safe to call unconditionally once `i2pd_init`/`i2pd_start` have run (guaranteed here,
        // since `RouterInner` is only ever constructed after `start` completes them).
        unsafe {
            i2pd_sys::i2pd_stop();
            i2pd_sys::i2pd_terminate();
        }
        ROUTER_STARTED.store(false, Ordering::Release);
    }
}

/// A running libi2pd router instance.
///
/// Cheaply [`Clone`]-able (an [`Arc`] handle internally): the underlying router is only actually
/// stopped and torn down once the last clone is dropped. Only one `I2pRouter` may be running per
/// process at a time -- see [`I2pError::AlreadyRunning`].
///
/// Unlike [`start`](Self::start) (which does its blocking libi2pd work on a `spawn_blocking`
/// worker), tearing down the *last* clone runs libi2pd's network-wide shutdown (stopping
/// transports/tunnels/netDb) synchronously on whatever thread drops it -- there is currently no
/// async `shutdown()`/`close()` alternative. If that last clone is dropped from within an async
/// task (including implicitly, e.g. a [`Destination`] or [`crate::I2pStream`] holding the only
/// remaining clone going out of scope at the end of a request handler), that blocks the executing
/// thread for as long as the shutdown takes. Prefer dropping the last `I2pRouter` clone (and
/// anything holding one) from a `spawn_blocking` context, or accept the stall, until an async
/// teardown path exists.
#[derive(Clone, Debug)]
pub struct I2pRouter {
    /// Held only for its `Drop` side effect (stopping/terminating libi2pd once the last clone
    /// goes away) -- never read directly.
    _inner: Arc<RouterInner>,
}

impl I2pRouter {
    /// Initializes and starts libi2pd's transport/tunnel/netDb subsystems. `app_name` controls
    /// the default data directory name libi2pd uses for its own files (router keys, netDb
    /// cache) -- distinct from any application-level persistent destination keys file, which
    /// this crate keeps entirely separate (see [`destination_from_keys_file`](Self::destination_from_keys_file)).
    ///
    /// Returns quickly (the actual network bootstrap continues on libi2pd's own background
    /// threads); creating destinations and connecting streams before the router has finished
    /// bootstrapping just takes longer; it does not fail outright.
    ///
    /// # Errors
    /// Returns [`I2pError::AlreadyRunning`] if an `I2pRouter` is already running in this
    /// process, or [`I2pError::InvalidAppName`] if `app_name` contains an interior NUL byte.
    pub async fn start(app_name: impl Into<String>) -> Result<Self, I2pError> {
        if ROUTER_STARTED.swap(true, Ordering::AcqRel) {
            return Err(I2pError::AlreadyRunning);
        }
        let app_name = app_name.into();
        let result = tokio::task::spawn_blocking(move || {
            let c_name = CString::new(app_name).map_err(|_| I2pError::InvalidAppName)?;
            // SAFETY: `i2pd_init` must run exactly once before any other i2pd-sys call, and the
            // `ROUTER_STARTED` compare-and-swap above guarantees that's true across the whole
            // process. `i2pd_start` is documented safe to call right after `i2pd_init`.
            unsafe {
                i2pd_sys::i2pd_init(c_name.as_ptr());
                i2pd_sys::i2pd_start();
            }
            Ok(())
        })
        .await;

        match result {
            Ok(Ok(())) => Ok(Self {
                _inner: Arc::new(RouterInner {
                    keys_file_lock: tokio::sync::Mutex::new(()),
                }),
            }),
            Ok(Err(e)) => {
                ROUTER_STARTED.store(false, Ordering::Release);
                Err(e)
            }
            Err(_) => {
                ROUTER_STARTED.store(false, Ordering::Release);
                Err(I2pError::WorkerPanicked)
            }
        }
    }

    /// Creates a new transient destination: a fresh, ephemeral keypair generated on the spot,
    /// published to the netDb for the lifetime of the returned [`Destination`] only. Reachable
    /// at a different `.b32.i2p` address every time this is called.
    ///
    /// # Errors
    /// Returns [`I2pError::DestinationCreationFailed`] if libi2pd fails to create it.
    pub async fn create_transient_destination(&self) -> Result<Destination, I2pError> {
        let router = self.clone();
        tokio::task::spawn_blocking(move || {
            // SAFETY: the router is running (this `I2pRouter` handle proves it); the returned
            // pointer (possibly null on failure) is immediately handed to `Destination::from_raw`,
            // which takes ownership and never touches it again on this thread.
            let ptr = unsafe { i2pd_sys::i2pd_create_transient_destination() };
            Destination::from_raw(router, ptr)
        })
        .await
        .map_err(|_| I2pError::WorkerPanicked)?
    }

    /// Generates a fresh persistent-destination keypair, serialized as an opaque byte buffer
    /// suitable for [`create_persistent_destination`](Self::create_persistent_destination) or for
    /// writing straight to disk. Most callers want
    /// [`destination_from_keys_file`](Self::destination_from_keys_file) instead, which handles
    /// generation-and-persistence automatically.
    ///
    /// # Errors
    /// Returns [`I2pError::KeyGenerationFailed`] if libi2pd fails to generate the keypair.
    pub async fn generate_keys(
        &self,
        sig: SigType,
        crypto: CryptoType,
    ) -> Result<Vec<u8>, I2pError> {
        tokio::task::spawn_blocking(move || {
            let mut buf: *mut u8 = std::ptr::null_mut();
            let mut len: usize = 0;
            // SAFETY: `out_buf`/`out_len` are valid, distinct, writable local variables; on
            // success the returned buffer is immediately copied out and freed via
            // `i2pd_free_buffer`, matching the shim's ownership contract.
            let ok = unsafe {
                i2pd_sys::i2pd_generate_keys(
                    sig.as_raw(),
                    crypto.as_raw(),
                    &raw mut buf,
                    &raw mut len,
                )
            };
            if ok == 0 || buf.is_null() {
                return Err(I2pError::KeyGenerationFailed);
            }
            // SAFETY: `buf`/`len` were just populated by a successful `i2pd_generate_keys` call.
            let bytes = unsafe { std::slice::from_raw_parts(buf, len) }.to_vec();
            // SAFETY: `buf` was allocated by `i2pd_generate_keys` and not freed yet.
            unsafe { i2pd_sys::i2pd_free_buffer(buf) };
            Ok(bytes)
        })
        .await
        .map_err(|_| I2pError::WorkerPanicked)?
    }

    /// Creates a destination from a keys buffer previously produced by
    /// [`generate_keys`](Self::generate_keys) (or read back from wherever it was persisted).
    /// Most callers want [`destination_from_keys_file`](Self::destination_from_keys_file) instead.
    ///
    /// `is_public` controls whether this destination's lease set is published to the netDb: a
    /// public destination (the common case -- pass `true`) is reachable by inbound
    /// [`accept`](Destination::accept) calls from other peers, the same way an eepsite needs to
    /// be found to be visited. A private destination (`false`) is never published and so can
    /// never receive inbound streams -- only useful for a destination that will exclusively make
    /// outbound [`connect`](Destination::connect) calls.
    ///
    /// # Errors
    /// Returns [`I2pError::DestinationCreationFailed`] if `keys` is malformed or libi2pd
    /// otherwise fails to create the destination.
    pub async fn create_persistent_destination(
        &self,
        keys: Vec<u8>,
        is_public: bool,
    ) -> Result<Destination, I2pError> {
        let router = self.clone();
        tokio::task::spawn_blocking(move || {
            // SAFETY: `keys` is a valid, non-empty (checked by the shim) byte buffer alive for
            // the duration of this call; the returned pointer is handed to `Destination::from_raw`.
            let ptr = unsafe {
                i2pd_sys::i2pd_create_persistent_destination(
                    keys.as_ptr(),
                    keys.len(),
                    std::os::raw::c_int::from(is_public),
                )
            };
            Destination::from_raw(router, ptr)
        })
        .await
        .map_err(|_| I2pError::WorkerPanicked)?
    }

    /// Loads a persistent destination's keys from `path`, generating and saving a fresh keypair
    /// there first if the file doesn't exist yet -- using `sig`/`crypto` for that first-time
    /// generation only (an existing file's keys keep whatever algorithms they were originally
    /// generated with; `sig`/`crypto` are simply ignored once a file already exists). Reusing the
    /// same path across restarts keeps the same `.b32.i2p` address -- the I2P equivalent of
    /// [`OnionConfig::state_dir`](https://docs.rs/tachyon-web/latest/tachyon_web/server/tor/struct.OnionConfig.html#method.state_dir)
    /// keeping the same `.onion` address.
    ///
    /// The keys file is written with no format guarantees beyond "whatever this crate's own
    /// version wrote" -- treat it as an opaque blob, back it up like any other private key
    /// material (anyone who obtains it can impersonate this destination), and don't hand-edit it.
    ///
    /// `is_public` is passed straight through to
    /// [`create_persistent_destination`](Self::create_persistent_destination) -- see its docs for
    /// what it controls. Note this applies on every load, not just first-time generation: loading
    /// an existing keys file with `is_public: false` still creates a private, unreachable
    /// destination even though the keys themselves are unchanged.
    ///
    /// Concurrent calls (on this `I2pRouter` or any of its clones) for the *same* `path` are
    /// serialized, so two tasks racing to create the same first-time destination can't each
    /// generate and persist a different keypair -- only one keypair is ever generated per path,
    /// and every caller waiting on that path gets a `Destination` matching what's on disk. This
    /// only guards against races within this process; two separate processes racing to write the
    /// same path can still each generate and write their own keys, same as any unsynchronized
    /// concurrent file write.
    ///
    /// # Errors
    /// Returns [`I2pError::Io`] if the file can't be read/written, or
    /// [`I2pError::DestinationCreationFailed`]/[`I2pError::KeyGenerationFailed`] per
    /// [`create_persistent_destination`](Self::create_persistent_destination)/[`generate_keys`](Self::generate_keys).
    pub async fn destination_from_keys_file(
        &self,
        path: impl Into<PathBuf>,
        is_public: bool,
        sig: SigType,
        crypto: CryptoType,
    ) -> Result<Destination, I2pError> {
        let path = path.into();
        let keys = {
            let _guard = self._inner.keys_file_lock.lock().await;
            match tokio::fs::read(&path).await {
                Ok(bytes) => bytes,
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
                    let generated = self.generate_keys(sig, crypto).await?;
                    if let Some(parent) = path.parent() {
                        tokio::fs::create_dir_all(parent)
                            .await
                            .map_err(I2pError::Io)?;
                    }
                    tokio::fs::write(&path, &generated)
                        .await
                        .map_err(I2pError::Io)?;
                    generated
                }
                Err(e) => return Err(I2pError::Io(e)),
            }
        };
        self.create_persistent_destination(keys, is_public).await
    }
}