Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ QR_WRAP := src/qr/qr.c
# FPU-less 68000 (quirc's identify geometry is float-heavy; the pair is what its
# docs recommend, and avoids slow double soft-float via rintf/sqrtf/fabsf).
QR_CPPFLAGS := -Isrc/qr -DQUIRC_FLOAT_TYPE=float -DQUIRC_USE_TGMATH

# --- Vendored QR encoder (qrcodegen, MIT) + our portable wrapper ------------
# qrcodegen is third-party (warnings off, same reasoning as quirc above); it's
# integer-only so no float flags are needed. Reuses the qr-host/qr-m68k object
# pattern rules below (they match any src/qr/%.c, qrcodegen.c included).
QRENC_WRAP := src/qr/qrencode.c
DIFF_SRCS := tests/diff/diff_main.c
# AmigaOS-only front-end glue (bsdsocket SNTP, ...); m68k build only. qrimage.c
# is GUI-only (datatypes.library) so it's excluded here and added to GUI_SRCS.
Expand All @@ -80,6 +86,10 @@ BUILD := build
QUIRC_HOST_OBJS := $(patsubst src/qr/%.c,$(BUILD)/qr-host/%.o,$(QUIRC_SRCS))
QUIRC_M68K_OBJS := $(patsubst src/qr/%.c,$(BUILD)/qr-m68k/%.o,$(QUIRC_SRCS))

# Object paths for the vendored qrcodegen encoder (same qr-host/qr-m68k rules).
QRCODEGEN_HOST_OBJ := $(BUILD)/qr-host/qrcodegen.o
QRCODEGEN_M68K_OBJ := $(BUILD)/qr-m68k/qrcodegen.o

.PHONY: all test cli smoke diff m68k m68k-docker gui gui-docker gui-smoke qr-onhw qr-onhw-docker qr-onhw-smoke serialtest-m68k serialtest-m68k-docker copperline-smoke pbkdf2-bench asm-bench amissl-bench clean

all: test cli
Expand All @@ -89,9 +99,10 @@ test: $(BUILD)/run-tests
VAULT_TEST_FILE=$(BUILD)/amiauth-test.vault \
AMIAUTH_PREFS_DIR=$(BUILD)/prefs-test $(BUILD)/run-tests

$(BUILD)/run-tests: $(CORE_SRCS) $(TEST_SRCS) $(QR_WRAP) $(QUIRC_HOST_OBJS) | $(BUILD)
$(BUILD)/run-tests: $(CORE_SRCS) $(TEST_SRCS) $(QR_WRAP) $(QUIRC_HOST_OBJS) $(QRENC_WRAP) $(QRCODEGEN_HOST_OBJ) | $(BUILD)
$(CC) $(CFLAGS) $(CObjINC) $(QR_CPPFLAGS) -Itests \
$(CORE_SRCS) $(TEST_SRCS) $(QR_WRAP) $(QUIRC_HOST_OBJS) -o $@
$(CORE_SRCS) $(TEST_SRCS) $(QR_WRAP) $(QUIRC_HOST_OBJS) \
$(QRENC_WRAP) $(QRCODEGEN_HOST_OBJ) -o $@

# Vendored quirc objects — host toolchain, warnings suppressed (third-party).
$(BUILD)/qr-host/%.o: src/qr/%.c | $(BUILD)
Expand All @@ -100,11 +111,13 @@ $(BUILD)/qr-host/%.o: src/qr/%.c | $(BUILD)

# --- Host: native CLI (for local development) ---
# Named distinctly from the m68k 'AmiAuth' binary so the two don't collide on a
# case-insensitive filesystem (macOS).
# case-insensitive filesystem (macOS). Includes the QR encoder (#45's CLI QR
# command): our qrencode.c wrapper + the vendored qrcodegen object.
cli: $(BUILD)/amiauth-host

$(BUILD)/amiauth-host: $(CORE_SRCS) $(CLI_SRCS) | $(BUILD)
$(CC) $(CFLAGS) $(CObjINC) $(CORE_SRCS) $(CLI_SRCS) -o $@
$(BUILD)/amiauth-host: $(CORE_SRCS) $(CLI_SRCS) $(QRENC_WRAP) $(QRCODEGEN_HOST_OBJ) | $(BUILD)
$(CC) $(CFLAGS) $(CObjINC) -Isrc/qr $(CORE_SRCS) $(CLI_SRCS) \
$(QRENC_WRAP) $(QRCODEGEN_HOST_OBJ) -o $@

# --- Host: end-to-end CLI smoke test (always-unlocked round trip) ---
smoke: $(BUILD)/amiauth-host
Expand All @@ -119,8 +132,11 @@ $(BUILD)/run-diff: $(CORE_SRCS) $(DIFF_SRCS) | $(BUILD)
$(OPENSSL_LIBS) -o $@

# --- m68k: Amiga CLI binary (amiga-gcc on PATH) ---
m68k: | $(BUILD)
$(M68K_CC) $(M68K_CFLAGS) $(CORE_SRCS) $(ASM_SRCS) $(AMIGA_SRCS) $(CLI_SRCS) -o $(BUILD)/AmiAuth
# Includes the QR encoder (#45's CLI QR command): see the qr-m68k object rule
# below (shared with the GUI's decoder build).
m68k: $(QRCODEGEN_M68K_OBJ) | $(BUILD)
$(M68K_CC) $(M68K_CFLAGS) -Isrc/qr $(CORE_SRCS) $(ASM_SRCS) $(AMIGA_SRCS) $(CLI_SRCS) \
$(QRENC_WRAP) $(QRCODEGEN_M68K_OBJ) -o $(BUILD)/AmiAuth

# --- m68k: ReAction GUI binary (Amiga only; needs intuition + ReAction classes) ---
# Includes the QR decoder: qrimage.c (datatypes glue) + our qr.c wrapper + the
Expand All @@ -132,9 +148,9 @@ $(BUILD)/qr-m68k/%.o: src/qr/%.c | $(BUILD)
@mkdir -p $(BUILD)/qr-m68k
$(M68K_CC) -std=c99 -O2 -m68000 -noixemul -w $(QR_CPPFLAGS) -c $< -o $@

gui: $(QUIRC_M68K_OBJS) | $(BUILD)
gui: $(QUIRC_M68K_OBJS) $(QRCODEGEN_M68K_OBJ) | $(BUILD)
$(M68K_CC) $(M68K_CFLAGS) $(VERSION_DEFS) $(QR_CPPFLAGS) $(CORE_SRCS) $(ASM_SRCS) $(AMIGA_SRCS) $(GUI_SRCS) \
$(QR_WRAP) $(QUIRC_M68K_OBJS) -lm -lamiga -o $(BUILD)/AmiAuthGUI
$(QR_WRAP) $(QUIRC_M68K_OBJS) $(QRENC_WRAP) $(QRCODEGEN_M68K_OBJ) -lm -lamiga -o $(BUILD)/AmiAuthGUI

gui-docker:
$(DOCKER) run --rm --platform linux/amd64 $(DOCKER_USER) -v "$(CURDIR)":/work -w /work \
Expand Down
21 changes: 20 additions & 1 deletion THIRDPARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

AmiAuth ships zero *mandatory runtime* dependencies: everything it needs is
either an AmigaOS-standard shared library or compiled into the binary from
source in this repository. The only vendored third-party source is:
source in this repository. The vendored third-party source is:

## quirc — QR-code recognition library

Expand All @@ -23,3 +23,22 @@ no dependencies of its own (standard C only) and is built with
of the program uses.

Its ISC licence text is preserved verbatim in every vendored source file.

## qrcodegen — QR-code generation library

- **Location:** [`src/qr/`](src/qr/) (`qrcodegen.h`, `qrcodegen.c`)
- **Upstream:** https://github.com/nayuki/QR-Code-generator (the `c/`
subdirectory), pinned to commit `2c9044de6b049ca25cb3cd1649ed7e27aa055138`.
Actively maintained, so unlike quirc no fork is tracked for this one.
- **Author:** Project Nayuki
- **License:** MIT (permissive; compatible with AmiAuth's BSD 2-Clause)

Used by the GUI and CLI to render an account's `otpauth://` URI as a QR code
for export (#45) — the encode-side counterpart to quirc's decode. It has no
dependencies of its own (standard C only), does no dynamic allocation (every
buffer is caller-supplied), and is integer-only (no floating point), so it
needs no special build flags for the plain-68000 target. The wrapper in
[`src/qr/qrencode.c`](src/qr/qrencode.c) (AmiAuth's own code) is the only
interface the rest of the program uses.

Its MIT licence text is preserved verbatim in every vendored source file.
9 changes: 6 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Resolves corrected UTC without touching the system clock (full design in
## Front-ends

- **CLI** — no GUI dependency at all; retains full code-generation on OS 2.x and
floppy-booted machines. Commands: `CODE`, `INIT`, `ADD`, `LIST`, `GET`,
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 in `src/amiga/` and is linked
into the m68k build only; the host build stubs it. When a GUI is resident the
Expand All @@ -73,9 +73,12 @@ Resolves corrected UTC without touching the system clock (full design in
- **GUI (ClassAct/ReAction)** — a multi-column `listbrowser.gadget` (all accounts
with live codes + countdown), a large selected-code display, a
`fuelgauge.gadget` countdown, add / remove / edit, clipboard copy (iffparse
FTXT, auto-clear), the clock-status LED, and **QR-image import** (decode an
FTXT, auto-clear), the clock-status LED, **QR-image import** (decode an
`otpauth://` QR from an image file via `datatypes.library` + a vendored quirc
decoder in `src/qr/`). Uses only classes common to ClassAct 3.3 and ReAction.
decoder in `src/qr/`) and **QR export** (render an account's `otpauth://`
URI as an on-screen QR code for a phone to scan, via a vendored qrcodegen
encoder also in `src/qr/`; CLI counterpart is `QR`, 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_PRIORITY` tooltypes, default hotkey `ctrl alt a`, window hides
(not quits) on close, full Exchange integration (show/hide/enable/disable/kill),
Expand Down
7 changes: 5 additions & 2 deletions src/amiga/guiport.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ enum {
AAP_SHOW, /* pop the GUI window to the front */
AAP_ADD_SECRET, /* arg = "issuer\nlabel\nbase32secret" (bare-secret ADD;
* issuer may be empty, the other two must not be) */
AAP_ADD_SECRET_STEAM /* same arg shape; builds a Steam Guard account
AAP_ADD_SECRET_STEAM, /* same arg shape; builds a Steam Guard account
* instead of an ordinary TOTP one (#44) */
AAP_QR /* arg = account name; reply buf = ASCII-art QR of the
* account's otpauth:// URI (#45) */
};

/* Result codes (aar_Result / *result). */
Expand All @@ -36,7 +38,8 @@ enum {
AAR_NOTFOUND, /* no account matched */
AAR_FULL, /* vault is full */
AAR_BADARG, /* e.g. not a valid otpauth:// URI */
AAR_SAVEFAIL /* mutation applied in memory but the re-save failed */
AAR_SAVEFAIL, /* mutation applied in memory but the re-save failed */
AAR_TOOLONG /* AAP_QR: the URI doesn't fit even at the QR version cap */
};

/* Try to forward a command to a resident GUI. Returns 0 if a GUI handled it
Expand Down
45 changes: 43 additions & 2 deletions src/cli/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* ADD <otpauth://...> import an account
* LIST list account names
* GET <account> print an account's current code
* QR <account> print an ASCII-art QR code
* REMOVE <account> delete an account
* HELP
*
Expand All @@ -29,6 +30,7 @@
#include "vault.h"
#include "pbkdf2.h"
#include "prefs.h"
#include "qrencode.h" /* qr_encode_uri/qr_render_ascii (#45) */

#if defined(__unix__) || defined(__APPLE__) || defined(__linux__)
# include <unistd.h>
Expand Down Expand Up @@ -611,15 +613,18 @@ static int cmd_init(const char *path, int always_unlocked, long iterations,
* the GUI handled it, or -1 if no GUI is running (do it locally). */
static int try_forward(int cmd, const char *arg)
{
static char buf[2600]; /* off the ~4 KB shell stack; holds a LIST reply */
/* Off the ~4 KB shell stack. Sized for the largest reply any forwarded
* command can produce: AAP_QR's ASCII-art render (QR_ENC_ASCII_BUF_LEN,
* ~11.9 KB) dwarfs LIST's "issuer:label\n" lines. */
static char buf[QR_ENC_ASCII_BUF_LEN];
int result = AAR_OK;

if (gui_forward(cmd, arg, buf, sizeof buf, &result) != 0)
return -1; /* no resident GUI -> local path */

switch (result) {
case AAR_OK:
if (buf[0]) fputs(buf, stdout); /* GET code / LIST names (with \n) */
if (buf[0]) fputs(buf, stdout); /* GET code / LIST names / QR art */
return 0;
case AAR_LOCKED:
fprintf(stderr, "AmiAuth: the running GUI's vault is locked; "
Expand All @@ -637,6 +642,9 @@ static int try_forward(int cmd, const char *arg)
case AAR_SAVEFAIL:
fprintf(stderr, "AmiAuth: applied in the GUI but the re-save failed\n");
return 2;
case AAR_TOOLONG:
fprintf(stderr, "AmiAuth: this account's URI is too long to encode as a QR code\n");
return 2;
default:
fprintf(stderr, "AmiAuth: the running GUI could not handle that\n");
return 2;
Expand Down Expand Up @@ -786,6 +794,36 @@ static int cmd_get(const char *path, const char *account)
return 0;
}

/* Print an account's otpauth:// URI as ASCII-art QR code (#45), for a phone
* to scan directly off a terminal. GUI counterpart: "Show QR code...". */
static int cmd_qr(const char *path, const char *account)
{
static vault v; /* ~19 KB: keep it off the (small, ~4 KB) AmigaShell stack */
static char uri[512];
static uint8_t modules[QR_ENC_MAX_MODULES * QR_ENC_MAX_MODULES];
static char ascii[QR_ENC_ASCII_BUF_LEN];
vault_result rc;
int idx, qsize;
int fc = try_forward(AAP_QR, account);
if (fc >= 0) return fc;
rc = open_vault(&v, path);
if (rc != VAULT_OK) { fprintf(stderr, "AmiAuth: %s\n", vault_err(rc)); return 2; }
idx = find_account(&v, account);
if (idx < 0) { vault_lock(&v); fprintf(stderr, "AmiAuth: no account matching '%s'\n", account); return 2; }

if (otpauth_build(&v.accounts[idx], uri, sizeof uri) != 0 ||
qr_encode_uri(uri, modules, &qsize) != QRENC_OK) {
vault_lock(&v);
fprintf(stderr, "AmiAuth: this account's URI is too long to encode as a QR code\n");
return 2;
}
qr_render_ascii(modules, qsize, ascii, sizeof ascii);
fputs(ascii, stdout);

vault_lock(&v);
return 0;
}

static int cmd_remove(const char *path, const char *account)
{
static vault v; /* ~19 KB: keep it off the (small, ~4 KB) AmigaShell stack */
Expand Down Expand Up @@ -822,6 +860,8 @@ static int usage(void)
" add STEAM for a Steam Guard code)\n"
" LIST List account names\n"
" GET <account> Print an account's code\n"
" QR <account> Print an ASCII-art QR code\n"
" of the account (for a phone)\n"
" REMOVE <account> Delete an account\n"
" SHOW Pop the running GUI to front\n"
" CLOCK Show UTC offset + status\n"
Expand Down Expand Up @@ -895,6 +935,7 @@ static int dispatch(const cli_args *a)
}
if (ci_streq(a->command, "LIST")) return cmd_list(vault);
if (ci_streq(a->command, "GET")) return a->value ? cmd_get(vault, a->value) : usage();
if (ci_streq(a->command, "QR")) return a->value ? cmd_qr(vault, a->value) : usage();
if (ci_streq(a->command, "REMOVE")) return a->value ? cmd_remove(vault, a->value) : usage();
if (ci_streq(a->command, "SHOW")) return cmd_show();
if (ci_streq(a->command, "CLOCK")) return cmd_clock();
Expand Down
27 changes: 27 additions & 0 deletions src/core/base32.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,30 @@ int base32_decode(const char *in, uint8_t *out, size_t outcap)
/* Any leftover < 8 bits are padding and discarded. */
return (int)n;
}

int base32_encode(const uint8_t *in, size_t inlen, char *out, size_t outcap)
{
static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
uint32_t buf = 0; /* bit accumulator (MSB-first) */
int bits = 0; /* valid bits currently in buf */
size_t n = 0;
size_t i;

if (!in || !out) return -1;

for (i = 0; i < inlen; i++) {
buf = (buf << 8) | in[i];
bits += 8;
while (bits >= 5) {
bits -= 5;
if (n + 1 >= outcap) return -1; /* leave room for the NUL */
out[n++] = alphabet[(buf >> bits) & 0x1f];
}
}
if (bits > 0) {
if (n + 1 >= outcap) return -1;
out[n++] = alphabet[(buf << (5 - bits)) & 0x1f];
}
out[n] = '\0';
return (int)n;
}
9 changes: 8 additions & 1 deletion src/core/base32.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* base32.h — RFC 4648 Base32 decode, tolerant of padding, whitespace and case. */
/* base32.h — RFC 4648 Base32 encode/decode, tolerant of padding, whitespace
* and case on decode. */
#ifndef AMIAUTH_BASE32_H
#define AMIAUTH_BASE32_H

Expand All @@ -10,4 +11,10 @@
* invalid character or if the result would exceed `outcap`. */
int base32_decode(const char *in, uint8_t *out, size_t outcap);

/* Encode `inlen` bytes of `in` into `out` (capacity `outcap`), NUL-terminated.
* Uppercase RFC 4648 alphabet, no '=' padding (matches how services usually
* display a secret). Returns the number of characters written (excluding the
* NUL), or -1 if the result (plus NUL) would exceed `outcap`. */
int base32_encode(const uint8_t *in, size_t inlen, char *out, size_t outcap);

#endif /* AMIAUTH_BASE32_H */
Loading