AmiAuth is split into a portable, testable core and Amiga-specific front-ends. Everything below the front-end line is plain C with no OS dependencies, so it builds and runs under a host test harness against the official RFC test vectors.
+---------------------------------------------+
| GUI (ClassAct/ReAction) | CLI front-end |
| + commodity shell | |
| (commodities.library: | |
| hotkey, Exchange) | |
+------------+-------------+---------+---------+
| |
+------------v-----------------------v---------+
| core: otp.c (TOTP/HOTP) |
| base32.c |
| uri.c (otpauth:// parsing) |
| vault.c (encrypted store) |
| clock.c (SNTP + offset resolution) |
+------------+---------------------------------+
|
sha1.c / hmac.c / chacha20.c / pbkdf2.c
(self-contained, no external crypto deps)
Self-contained, vendored implementations. No external crypto dependency (no AmiSSL requirement). SHA-1/HMAC power both OTP generation and the vault MAC; ChaCha20 is the vault cipher; PBKDF2-HMAC-SHA1 is the KDF.
RFC 6238 / RFC 4226: HMAC-SHA1 over an 8-byte counter, dynamic truncation, modulo to 6 or 8 digits. Configurable period and T0.
RFC 4648 Base32 decode, padding-tolerant and whitespace/case insensitive — pasted secrets are messy in practice.
otpauth:// URI parsing (issuer, label, secret, algorithm, digits, period),
so a secret exported from another authenticator can be pasted directly.
Multi-account store, encrypted at rest. File header stores KDF salt + calibrated
iteration count + cipher marker. Construction is encrypt-then-MAC (ChaCha20 +
HMAC-SHA1). Supports an always-unlocked mode (cipher marked none) using the
same file format. The on-disk layout is frozen in VAULT_FORMAT.md.
vault.c takes an explicit file path and is unaware of PROGDIR:/ENVARC: —
those live in the Amiga front-end; see STORAGE.md for where the
vault and settings are kept and why. See also SECURITY.md.
Resolves corrected UTC without touching the system clock (full design in CLOCK.md), in priority order:
- SNTP — a single UDP exchange over
bsdsocketif a TCP/IP stack is up; compute offset vs system clock, use corrected time, display measured offset. - Explicit UTC offset — user-stated offset for offline machines.
- Manual nudge — a ±seconds control, synced by eye against the countdown.
- CLI — no GUI dependency at all; retains full code-generation on OS 2.x and
floppy-booted machines. Commands:
CODE,INIT,ADD,LIST,GET,QR,REMOVE,SHOW,CLOCK,SYNC,OFFSET. Amiga-only front-end glue (bsdsocket SNTP, entropy, the GUI-forward client, ...) lives insrc/amiga/and is linked into the m68k build only; the host build stubs it. When a GUI is resident the CLI forwards vault commands to it over a public message port (src/amiga/guiport.[ch]) instead of opening the vault a second time. - GUI (ClassAct/ReAction) — a multi-column
listbrowser.gadget(all accounts with live codes + countdown), a large selected-code display, afuelgauge.gadgetcountdown, add / remove / edit, clipboard copy (iffparse FTXT, auto-clear), the clock-status LED, QR-image import (decode anotpauth://QR from an image file viadatatypes.library+ a vendored quirc decoder insrc/qr/) and QR export (render an account'sotpauth://URI as an on-screen QR code for a phone to scan, via a vendored qrcodegen encoder also insrc/qr/; CLI counterpart isQR, both share the encoder and its ASCII-art renderer). Uses only classes common to ClassAct 3.3 and ReAction. - Commodity shell — runs resident via
commodities.library:CX_POPKEY/CX_POPUP/CX_PRIORITYtooltypes, default hotkeyctrl alt a, window hides (not quits) on close, full Exchange integration (show/hide/enable/disable/kill), single-instance. The unlocked vault is held while resident (with optional idle auto-lock), so a hotkey pop-up or a forwarded CLI command needs no re-prompt.
- Host — portable core compiled natively for CI. Runs RFC 6238 Appendix B and RFC 4226 Appendix D vectors without an emulator.
- m68k —
amiga-gcc. Plain 68000 (-m68000) is the baseline target: all code (CLI and GUI) must build and run on a stock 68000 to maximise reach. Requiring 020+ needs a very good reason; the only 020+-specific code path left is the optional AmiSSL provider (#85, not yet implemented), gated by runtime detection and never required.
sha1_compress() and chacha20_block() (HMAC and PBKDF2 have no hot loops of
their own - they're built entirely on SHA-1) are reached through a
function-pointer seam (src/core/crypto_dispatch.h), defaulting to the
portable C reference. On Amiga, src/amiga/crypto_select.c repoints SHA-1 at
hand-written m68k assembly (src/core/sha1_asm.s) at startup, unless
ENVARC:AmiAuth/cryptoasm=off forces the C reference back on.
Unlike a typical "020+ accelerated path", this asm is restricted to
instructions available on the plain 68000 baseline (verified in CI under
amitools' vamos, on both -C 000 and -C 020), so it's the default on
every CPU tier this project supports, not an opt-in extra - 68020+ still runs
it faster purely by being a faster CPU on the same instructions. Faster
primitives convert directly into security margin, since PBKDF2 iterations are
calibrated to ~1s of wall time.
Measured under Copperline (PAL EClock, real Kickstart 3.1 ROM), timing PBKDF2-HMAC-SHA1 (600 iterations) on each CPU tier Copperline models:
| CPU | C reference | asm | asm speedup |
|---|---|---|---|
| 68000 | 13.65 iters/s | 15.97 iters/s | ~17.0% |
| 68020 | 46.28 iters/s | 55.55 iters/s | ~20.1% |
| 68030 | 61.46 iters/s | 70.87 iters/s | ~15.3% |
The advantage holds at every tier - the "faster CPU, same instructions" claim above is measured, not assumed.
ChaCha20 has no asm path: a hand-written attempt measured ~17% slower than
its C reference on the same real-hardware setup - the naive per-quarter-round
design reloads all 16 state words from the stack every round instead of
keeping them register-resident the way GCC's -O2 output does, and closing
that gap needs a substantially more involved rewrite that wasn't judged worth
the risk for this pass. g_chacha20_block stays on the C default; see
src/amiga/crypto_select.c and src/core/crypto_dispatch.h.
SHA-256/SHA-512 (sha256.c/sha512.c, added for the RFC 6238 algorithm
variants in #43) and Steam Guard's renderer (steamguard.c, #44) deliberately
have no asm path either, but for a different reason than ChaCha20: they aren't
hot loops in the first place. Unlike SHA-1, which backs PBKDF2's
iteration-heavy vault KDF (the table above), SHA-256/512 and Steam Guard each
compute at most one HMAC per TOTP code render - seconds apart, not thousands
of iterations back-to-back - so there's no wall-clock bottleneck for
hand-written asm to fix, and no measured case (per the ChaCha20 lesson above)
to justify writing one speculatively. Steam Guard's HMAC-SHA1 truncation
already goes through the existing g_sha1_compress dispatch at no extra cost.
Revisit only if either primitive ends up in a genuine hot loop, the same way
ChaCha20's asm case was decided on real-hardware measurement rather than
assumption.
An optional AmiSSL-backed provider (CRYPTO=BUILTIN|AMISSL|AUTO) plugging
into the same dispatch seam is tracked separately as #85 - AmiSSL itself
requires AmigaOS 3.0+/68020+, so unlike the asm above it can only ever sit
behind that gate.