tachyon-i2p 0.0.2

Safe async wrapper around i2pd-sys (native I2P `.b32.i2p` eepsite support).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! [`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,
        }
    }
}

/// An encryption algorithm a destination's LeaseSet2 can advertise as usable -- see
/// [`I2pRouter::create_persistent_destination`]'s `encryption_types` parameter.
///
/// Corresponds to libi2pd's stable, protocol-level `CryptoKeyType` enum (`libi2pd/Identity.h`).
/// This is *only* about the destination's advertised encryption capability -- not its identity
/// certificate, which is always ElGamal regardless (see
/// [`I2pRouter::generate_keys`]) -- so passing e.g. [`ElGamal`](Self::ElGamal) here doesn't mean
/// "an old-style ElGamal-only destination"; it's simply one entry alongside whatever else you
/// list.
///
/// [`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. Note
/// that passing an empty `encryption_types` slice (the common case) already gets you a hybrid
/// ElGamal+ECIES-X25519(+ML-KEM-768, if this crate was built against a post-quantum-capable
/// crypto backend) set automatically -- you only need to reach for explicit values here to
/// *narrow* that down (e.g. classical-only for a smaller LeaseSet2/older-client compatibility),
/// not to opt into the default hybrid behavior.
/// [`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) -> 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. The crypto-type arg is
            // ignored by the shim (see its doc comment) -- ElGamal is passed only as a clear,
            // self-documenting placeholder.
            let ok = unsafe {
                i2pd_sys::i2pd_generate_keys(
                    sig.as_raw(),
                    CryptoType::ElGamal.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.
    ///
    /// `encryption_types` controls which encryption algorithm(s) this destination's LeaseSet2
    /// advertises as usable (this is the parameter that actually matters for the trade-offs
    /// described on [`CryptoType`]'s docs -- unlike the identity certificate, which is always
    /// ElGamal regardless, see [`generate_keys`](Self::generate_keys)). An empty slice (the
    /// common case) requests libi2pd's own automatic default: the full hybrid set it derives
    /// from context -- ElGamal + ECIES-X25519-AEAD, plus ML-KEM-768+X25519 when this crate was
    /// built against a post-quantum-capable crypto backend. Passing one or more types instead
    /// (e.g. `&[CryptoType::EciesX25519]` for a classical-only destination with a smaller
    /// LeaseSet2 and maximum compatibility with older clients) publishes *exactly* that set, with
    /// no automatic extras -- the first entry is the preferred type, tried first by clients that
    /// support it.
    ///
    /// # 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,
        encryption_types: &[CryptoType],
    ) -> Result<Destination, I2pError> {
        let router = self.clone();
        let csv = if encryption_types.is_empty() {
            None
        } else {
            Some(
                encryption_types
                    .iter()
                    .map(|c| c.as_raw().to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            )
        };
        let csv = csv.map(|s| std::ffi::CString::new(s).unwrap_or_default());
        tokio::task::spawn_blocking(move || {
            let csv_ptr = csv.as_deref().map_or(std::ptr::null(), |c| c.as_ptr());
            // SAFETY: `keys` is a valid, non-empty (checked by the shim) byte buffer alive for
            // the duration of this call; `csv` (if present) is a valid NUL-terminated C string
            // alive for the duration of this call too; 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),
                    csv_ptr,
                )
            };
            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` for that first-time generation
    /// only (an existing file's keys keep whatever signature algorithm they were originally
    /// generated with; `sig` is 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`/`encryption_types` are passed straight through to
    /// [`create_persistent_destination`](Self::create_persistent_destination) -- see its docs for
    /// what they control. Note both apply on *every* load, not just first-time generation:
    /// loading an existing keys file with a different `encryption_types` re-publishes that
    /// destination's LeaseSet2 with the newly requested set (the keys themselves, and therefore
    /// the `.b32.i2p` address, are unaffected either way).
    ///
    /// 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,
        encryption_types: &[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).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, encryption_types)
            .await
    }
}