Files
Discord-Authenticator/crates/core/src/lib.rs
T
Miles WardandClaude Opus 4.8 f33effe20a v0.1: self-hosted TOTP authenticator with live Discord delivery
Rust workspace serving a Google/Microsoft-Authenticator-style web UI, with
each account's rotating code mirrored into Discord as a message that
rewrites itself in place as the code rolls.

The live mechanism is webhook message editing: POST ?wait=true returns the
message id, PATCH .../messages/{id} rewrites it thereafter. Two approaches
were evaluated and rejected — an externally-hosted image URL is cached by
Discord's media proxy and, worse, must be anonymously reachable, which would
publish live codes on an unauthenticated endpoint; and an animated countdown
drifts out of phase because playback starts at decode time, not on a wall
clock. The countdown instead uses Discord's <t:unix:R> markup, which the
client re-renders every second for free.

Codes are also sent as text in a fenced block so they stay copyable, with
the grouped, readable form on a rendered WebP tile.

- core: RFC 6238 TOTP over HMAC, lenient otpauth:// parsing (issuer conflicts
  and non-canonical base32 both occur in the wild), XChaCha20-Poly1305 vault,
  Argon2id admin auth
- discord: webhook client with a serialising rate limiter (~2 req/s) so
  globally-aligned rollovers don't burst, and error classification that
  distinguishes a deleted message (repost) from a dead webhook (stop)
- render: SVG -> WebP tiles via resvg with embedded JetBrains Mono
- http-api: axum, session auth, QR decode with a rescale/contrast/rotation
  retry ladder
- webui: offline vanilla JS, Beautiful UI design language, light and dark

Ships as a 27 MB Alpine image (about 8 MB to pull); the binary is fully static musl with no
OpenSSL and no C dependencies, cross-compiled via cargo-zigbuild so building
linux/amd64 on Apple Silicon needs no QEMU.

101 tests, including the RFC 6238 vectors, QR round-trips, and the live push
cycle exercised against a mock Discord.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-28 16:06:08 -04:00

44 lines
1.6 KiB
Rust

//! Domain core for Discord Authenticator.
//!
//! Deliberately free of network I/O — this crate owns the TOTP algorithm,
//! the encrypted vault, admin credentials and settings, and nothing else.
//! Talking to Discord lives in `da-discord`, drawing tiles in `da-render`,
//! and HTTP in `da-http-api`, so the security-critical parts here can be
//! tested without a socket in sight.
#![forbid(unsafe_code)]
pub mod auth;
pub mod crypto;
pub mod error;
pub mod settings;
pub mod totp;
pub mod vault;
pub use auth::{AdminAccount, AdminPublic, AdminStore};
pub use crypto::{Sealed, VaultKey};
pub use error::{Error, Result};
pub use settings::{validate_webhook_url, PushMode, Settings, SettingsStore};
pub use totp::{decode_secret, encode_secret, parse_otpauth, Algorithm, OtpAuth, Totp};
pub use vault::{Account, AccountPublic, DiscordState, PushTarget, Vault};
/// Seconds since the Unix epoch.
///
/// TOTP is defined against wall-clock time, so this is one of the few places
/// a monotonic clock is the wrong tool: if the operator's clock is corrected,
/// the codes must follow it.
pub fn unix_now() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
/// Milliseconds since the Unix epoch — sent to the browser so its countdown
/// tracks the server's clock rather than the viewing device's.
pub fn unix_now_millis() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis() as u64)
}