qualia_transport/lib.rs
1// Copyright (C) 2026 Qualia2FA contributors
2//
3// This library is free software; you can redistribute it and/or modify it under
4// the terms of the GNU Lesser General Public License as published by the Free
5// Software Foundation; either version 3 of the License, or (at your option) any
6// later version.
7//
8// This library is distributed in the hope that it will be useful, but WITHOUT
9// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
11// details.
12//
13// You should have received a copy of the GNU Lesser General Public License
14// along with this library; if not, see <https://www.gnu.org/licenses/>.
15
16//! **Transport channels** for Qualia2FA.
17//!
18//! This crate does one thing: it hands a byte string out and takes a byte string back. The
19//! protocol itself — whose payload arrives first, how conflicts are reported, how replays
20//! are blocked — lives in `qualia2fa-core`, and this crate knows nothing about it. The
21//! interface between the two is the core library's `Transport` trait, and the **dependency
22//! direction is "this crate ← core library"**: any project that needs to "put bytes into a
23//! QR code" or "find a peer on the LAN" can use this crate as is, without accepting
24//! Qualia2FA's vault format or account model.
25//!
26//! # What is here
27//!
28//! | Module | Contents |
29//! |--------|----------|
30//! | [`qr`] | QR capacity model, encoding, rendering and (optional) image decoding |
31//! | [`envelope`] | Binary payload ↔ text a QR code can carry |
32//! | [`frame`] | Animated QR: a payload that does not fit is split into several codes, which the scanning side reassembles |
33//! | [`image`] | The pixel form passed between rendering and scanning |
34//! | [`lan`] | LAN: multicast lobby (discovery), connection info and (optional) QUIC data plane |
35//!
36//! # Payloads are carried as text
37//!
38//! QR byte mode can carry arbitrary bytes, but then **only this crate could read the code
39//! back**: platform cameras (Android ML Kit, iOS AVFoundation) return strings, a clipboard
40//! holds text, and a human copying a code reads text. Payloads therefore go into a text
41//! envelope and are encoded in the QR **alphanumeric mode** — about 10% of the capacity, in
42//! exchange for every scanner returning the same string. See the module documentation of
43//! [`envelope`].
44//!
45//! The frame index and the frame count are part of that envelope from the start
46//! (`Q2FA1:2/3:`), so carrying a payload in several codes needs no additional wire format.
47//! Splitting and reassembly live in [`frame`], and the layer that turns a split payload into
48//! images lives in [`qr::QrProfile::encode_payload_frames`].
49//!
50//! # QR code and LAN
51//!
52//! The two channels do **not** replace each other:
53//!
54//! - The QR code has very little bandwidth, is one-shot and requires both ends to be
55//! present, but it needs no network infrastructure and no permissions (Android 17's
56//! `ACCESS_LOCAL_NETWORK` and the iOS multicast entitlement do not apply to it).
57//! - The LAN has ample bandwidth and can be reused, but it requires both ends in the same
58//! broadcast domain, and multicast is disabled on many corporate WiFi networks.
59//!
60//! The combination is: **which route carries discovery is decided on the spot, the payload
61//! itself takes whichever route has enough bandwidth**. The core library's `SharePlan` makes
62//! that decision; this crate provides both routes.
63//!
64//! The two halves meet where the LAN route needs the QR channel: when multicast is
65//! unavailable, a connection record travels out of band in a QR code. That conversion lives in
66//! [`lan`] — [`lan::LanAddress::to_qr_symbols`] renders a record into codes through the same
67//! capacity and framing path as any other payload, [`lan::LanAddressScanner`] collects them
68//! again — so a caller does not assemble it from [`qr`] and [`frame`] on its own.
69//!
70//! # Not implemented
71//!
72//! The per-platform LAN permission flows (Android 17's `ACCESS_LOCAL_NETWORK`, iOS
73//! `NSLocalNetworkUsageDescription` and the multicast entitlement) belong to the platform
74//! integration layer and are out of scope here. The LAN path's "no internet" property
75//! currently goes as far as "no relay is built, no DNS/pkarr registration, HTTPS probing
76//! disabled"; **packet-capture verification has not been done**, so nothing beyond that may
77//! be claimed publicly (see the `iroh-lan` section of the `README`).
78//!
79//! # Purity
80//!
81//! [`qr`], [`envelope`], [`frame`], [`image`] and [`lan::wire`] are pure logic: no file
82//! system, no network, no clock, no environment variables; every input is a parameter. Hence
83//! `#![forbid(unsafe_code)]` holds, and no input may cause a panic — the only permitted way
84//! to fail is returning an [`Error`].
85//!
86//! The same contract covers panics inside **dependencies**, at the cost of one convergence
87//! point: every call into the image decoder (`rqrr`) from `qr::scan` is wrapped in
88//! `catch_unwind`, which turns an upstream assertion failure into
89//! [`Error::QrDecoderPanicked`]. That is **containment, not a fix** (the fix belongs
90//! upstream), and it relies on unwinding: on a platform side built with `panic = "abort"`,
91//! this layer stops working.
92//!
93//! [`lan::lobby`] is the **only** module that touches I/O (a UDP socket and one background
94//! thread), and the decisions inside it are kept to a minimum: all the reasoning lives in
95//! [`lan::wire`], so properties such as "three devices converge on one view" can be verified
96//! in a single process, without a network.
97//!
98//! # Supported toolchains
99//!
100//! The default build supports **Rust 1.89** (the manifest's `rust-version`). Enabling the
101//! default-off `iroh-lan` feature raises that floor to **1.91**, the `rust-version` iroh 1.3.0
102//! declares; Cargo has one `rust-version` per package and no per-feature equivalent, so that
103//! field states the default build's floor and the feature's floor is documented in the
104//! manifest and in the `lan::transport` module (which exists only with that feature).
105
106#![forbid(unsafe_code)]
107#![warn(missing_docs)]
108#![warn(clippy::missing_docs_in_private_items)]
109
110pub mod envelope;
111pub mod error;
112pub mod frame;
113pub mod image;
114pub mod lan;
115pub mod qr;
116
117/// Base32 per RFC 4648 §6.
118///
119/// Deliberately not a dependency from crates.io: the codec layer of this crate thus stays
120/// **zero-dependency**, and this implementation matches the semantics of the core library's
121/// `base32` (case-insensitive, padding tolerated, non-canonical trailing bits rejected), so
122/// that one RFC does not grow two behaviours across two repositories.
123mod base32;
124
125pub use error::{Error, Result};