Skip to main content

qualia2fa_core/
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//! The Qualia2FA core library.
17//!
18//! This crate carries the two-factor authentication (2FA) core logic of Qualia2FA and
19//! deliberately contains no user interface or command-line entry point, so that front
20//! ends such as a GUI, a CLI, or a mobile application can each depend on this library
21//! as a separate crate.
22//!
23//! # Purity contract
24//!
25//! This library is a pure logic library with **no I/O and no clock**:
26//!
27//! - It does not access the file system, the network, or environment variables, and it
28//!   does not read the system clock.
29//! - The current time is passed in by the caller as `u64` Unix seconds.
30//! - Randomness comes only from [`getrandom`], never through a user-space PRNG.
31//! - [`#![forbid(unsafe_code)]`](self) — the whole crate contains no `unsafe`.
32//!
33//! # Status
34//!
35//! Implemented: Base32 (RFC 4648), HOTP (RFC 4226), TOTP (RFC 6238), Steam Guard, mOTP,
36//! Yandex OTP, `otpauth://` parsing, the encrypted vault, recovery files, device pairing,
37//! and the sync protocol.
38//!
39//! # Usage
40//!
41//! ```
42//! // Version number.
43//! assert_eq!(qualia2fa_core::VERSION, env!("CARGO_PKG_VERSION"));
44//!
45//! // Base32 decoding (RFC 4648); tolerates lowercase, missing padding, and whitespace.
46//! use qualia2fa_core::{base32, ExposeSecret};
47//! let seed = base32::decode("MZXW6YTBOI======").expect("valid Base32");
48//! assert_eq!(seed.expose_secret(), b"foobar");
49//! ```
50//!
51//! # License
52//!
53//! Licensed under the GNU Lesser General Public License v3.0 or later
54//! (`LGPL-3.0-or-later`); see `COPYING.LESSER` and `LICENSE` in the repository root.
55
56#![forbid(unsafe_code)]
57#![warn(missing_docs)]
58#![warn(clippy::missing_docs_in_private_items)]
59
60pub mod auth;
61pub mod base32;
62mod error;
63pub mod export;
64pub mod flow;
65pub mod import;
66pub mod lock;
67pub mod motp;
68pub mod otp;
69pub mod pairing;
70pub mod portable;
71pub mod recovery;
72mod secret;
73pub mod sync;
74pub mod transport;
75pub mod uri;
76pub mod vault;
77pub mod yaotp;
78
79pub use auth::{Responder, Verified, Verifier};
80pub use error::{Error, Result};
81
82/// Public types of the registration and login orchestration.
83///
84/// Only the **user journey** layer and the two injection points are exported. No
85/// intermediate type split into steps is exported: a caller may legally drive
86/// `vault`/`pairing`/`device` in order by itself, but doing so bypasses the ordering
87/// this module guarantees (see the module documentation of [`flow`]), so the public
88/// surface does not encourage it.
89pub use flow::{DeviceKeys, Joiner, Login, Registered, SecureStore};
90
91/// Public types of device identity.
92///
93/// Device identity lives in [`qualia_dik`] — an independent, reusable crate that any
94/// project needing "a pair of P-256 identity keys in platform hardware" can use
95/// without accepting this crate's vault format or sync protocol. They are re-exported
96/// here so that downstream code does not have to depend on both crates.
97///
98/// `DevicePublicKey` and `DeviceSignature` are **not secret** — they are meant to be
99/// printed on QR codes, written to logs, and propagated between devices — so they are
100/// exported separately from the secret types, which makes it visible in an import
101/// list which types may be printed freely.
102pub use qualia_dik::{DeviceId, DevicePublicKey, DeviceSignature};
103
104/// Public types of the pairing protocol.
105///
106/// Only "what each side needs" is exported: the QR payload, the sessions in both
107/// directions, and the result. Internal session keys and confirmation-tag computation
108/// are not exported — they are part of the protocol invariants and must not be
109/// recombined by upper layers into other uses.
110pub use pairing::{Adoption, JoinerSession, NonceLog, PendingSession, QrPayload};
111
112/// Public types of the recovery key.
113pub use recovery::{KdfParams, export as export_recovery, import as import_recovery};
114
115/// Public types of the application lock's password wrap.
116///
117/// The wrap has its own file format and its own KDF domain; see [`lock`]. The
118/// platform-authentication wrap is not here: it is a separate library, because the
119/// key never leaves the platform and this crate touches no platform API.
120pub use lock::{
121    FILE_LEN as LOCK_WRAP_LEN, LockMarker, LockPending, WrapHeader, unwrap as unwrap_lock_password,
122    wrap as wrap_lock_password,
123};
124
125/// Public types of the transport layer.
126///
127/// Only the **protocol** half: capability declaration, the channel contract, the
128/// session, and its result type. Concrete channels (LAN, QR code, hotspot) implement
129/// [`Transport`] on the device side — they all need I/O and platform APIs, and this
130/// crate touches neither.
131///
132/// Deliberately does **not** export any intermediate type "split into steps": a caller
133/// either uses [`SyncSession`] or assembles the parts in `sync` itself. The former
134/// guarantees the ordering, the latter does not.
135pub use transport::{
136    Recipient, Roster, SharePlan, SyncOutcome, SyncSession, Transport, TransportCapabilities,
137};
138
139pub use secret::{
140    ExposeSecret, ExposeSecretMut, SecretBox, SecretBytes, SecretSlice, SecretString,
141};
142
143/// Re-exports the OS CSPRNG so that upper layers (FFI / device side) use the **same**
144/// randomness implementation.
145///
146/// This crate is the only place that declares a `getrandom` dependency, so upper layers
147/// take randomness from here instead of declaring it again — separate declarations
148/// would let the two version requirements drift apart and end up with two different
149/// CSPRNG wrappers in one binary.
150///
151/// Upper layers call `qualia2fa_core::getrandom::fill(&mut buf)`.
152///
153/// Note: prefer the high-level APIs this library already provides when random bytes are
154/// needed; calling this directly is the opening left for cases such as "generate a new
155/// seed", where the upper layer must decide length and purpose itself.
156pub use getrandom;
157
158/// Version of this library, taken from `package.version` in `Cargo.toml`.
159pub const VERSION: &str = env!("CARGO_PKG_VERSION");
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn version_starts_with_a_digit() {
167        assert!(
168            VERSION.starts_with(char::is_numeric),
169            "bad version: {VERSION}"
170        );
171    }
172
173    #[test]
174    fn version_has_three_semver_components() {
175        assert_eq!(VERSION.split('.').count(), 3, "bad version: {VERSION}");
176    }
177}