diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1d63027 --- /dev/null +++ b/.clang-format @@ -0,0 +1,16 @@ +# Config tuned to the existing hand-written style: +# 4-space indent, spaces only, K&R braces, 100-col. +# The Align* + SortIncludes:false options preserve the author's manual +# column alignment (aligned #defines, struct members, tables) as much as +# clang-format allows. Residual churn is alignment inside call-args and +# brace-init tables, which clang-format cannot preserve. +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +SortIncludes: false +AlignConsecutiveMacros: AcrossComments +AlignConsecutiveDeclarations: true +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: None diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10a27fc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Pin clang-format to match the version used locally. shellcheck is + # preinstalled on GitHub-hosted ubuntu runners. + - run: pipx install clang-format==19.1.7 + - run: ./ci --action=check + + test: + runs-on: ubuntu-latest + steps: + # Submodules (qrcodegen/littlefs) are needed for the coverage build. + - uses: actions/checkout@v4 + with: + submodules: recursive + # gcc/make are preinstalled; valgrind + lcov are not. + - run: sudo apt-get update && sudo apt-get install -y valgrind lcov + # Hard gate: ASan+UBSan + Valgrind. Coverage is report-only. + - run: ./ci --check=test --action=check diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..ef5c605 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,49 @@ +name: pages + +# Publishes the host-test coverage HTML report (test/coverage/html/) to GitHub +# Pages. Kept separate from ci.yml so the coverage gate and the publish step do +# not entangle. The report is regenerated on every push to the branches below. +on: + push: + branches: [chore/clang-format, chore/host-tests, master] + +# Least privilege for the Pages deployment via the job's OIDC token. +permissions: + contents: read + pages: write + id-token: write + +# Serialize deployments; do not cancel an in-flight publish. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + # Submodules (qrcodegen/littlefs) are needed for the coverage build. + - uses: actions/checkout@v4 + with: + submodules: recursive + # gcc/make are preinstalled; lcov (and valgrind) are not. + - run: sudo apt-get update && sudo apt-get install -y valgrind lcov + # Default action=fix: run harnesses + (re)generate test/coverage/html/. + - run: ./ci --check=test + # Enable Pages via the job token (no admin CLI scope needed). + - uses: actions/configure-pages@v5 + with: + enablement: true + - uses: actions/upload-pages-artifact@v3 + with: + path: test/coverage/html + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 5499017..c303104 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ build .vscode -*.uf2 \ No newline at end of file +*.uf2 + +# host test artifacts +test/coverage/ +*.gcno +*.gcda \ No newline at end of file diff --git a/build.sh b/build.sh index 8efc766..23c49a9 100755 --- a/build.sh +++ b/build.sh @@ -25,7 +25,7 @@ fi mkdir -p build cd build cmake -DPICO_BOARD=pico_w .. -make -j$(nproc) +make -j"$(nproc)" cd .. cp build/hslock.uf2 hslock.uf2 diff --git a/ci b/ci new file mode 100755 index 0000000..0eef3c4 --- /dev/null +++ b/ci @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Project CI entrypoint. Default: fix formatting locally. +# Use --action=check (dry-run, non-zero on diff) in a git hook / CI. +set -euo pipefail +cd "$(dirname "$0")" + +check=lint action=fix +for a in "$@"; do case $a in + --check=*) check=${a#*=} ;; + --action=*) action=${a#*=} ;; + *) echo "usage: ./ci [--check=lint] [--action=fix|check]" >&2; exit 2 ;; +esac; done + +srcs() { git ls-files '*.c' '*.h' ':!:libs/**' ':!:version.h'; } +shells() { git ls-files ':!:libs/**' | while read -r f; do + if head -1 -- "$f" | grep -q '^#!.*sh'; then printf '%s\n' "$f"; fi + done; } + +case $check.$action in + lint.fix) srcs | xargs -r clang-format -i + shells | xargs -r shellcheck ;; # no autofix; reports only + lint.check) srcs | xargs -r clang-format --dry-run --Werror + shells | xargs -r shellcheck ;; + # Host tests. The hard gate is ASan+UBSan + Valgrind (correctness); coverage + # is report-only (whole-codebase denominator keeps line% intentionally low), + # so it never fails CI. + test.check) make -C test asan + make -C test valgrind + make -C test coverage ;; + test.fix) make -C test coverage ;; # run + (re)gen report + *) echo "unknown --check=$check --action=$action" >&2; exit 2 ;; +esac diff --git a/hardware/buzzer.h b/hardware/buzzer.h index dfc8578..f47a8d4 100644 --- a/hardware/buzzer.h +++ b/hardware/buzzer.h @@ -3,9 +3,9 @@ #define BUZZER_PIN 17 -#define BUZZER_SHORT_BEEP_DELAY 100 +#define BUZZER_SHORT_BEEP_DELAY 100 #define BUZZER_MEDIUM_BEEP_DELAY 500 -#define BUZZER_LONG_BEEP_DELAY 2000 +#define BUZZER_LONG_BEEP_DELAY 2000 void buzzer_init(); void buzzer_on(); diff --git a/hardware/clock.c b/hardware/clock.c index 0548bf8..0cebab0 100644 --- a/hardware/clock.c +++ b/hardware/clock.c @@ -2,7 +2,8 @@ uint32_t clock_get_unix_time(void) { datetime_t dt; - if (!rtc_get_datetime(&dt)) return 0; + if (!rtc_get_datetime(&dt)) + return 0; struct tm t = { .tm_year = dt.year - 1900, @@ -17,12 +18,12 @@ uint32_t clock_get_unix_time(void) { } void clock_set_from_unix_time(uint32_t unix_time) { - time_t t = (time_t)unix_time; + time_t t = (time_t)unix_time; struct tm *utc = gmtime(&t); datetime_t dt = { .year = utc->tm_year + 1900, - .month = utc->tm_mon + 1, + .month = utc->tm_mon + 1, .day = utc->tm_mday, .dotw = utc->tm_wday, .hour = utc->tm_hour, diff --git a/hardware/keypad.c b/hardware/keypad.c index 04d1b71..10c2cb6 100644 --- a/hardware/keypad.c +++ b/hardware/keypad.c @@ -2,12 +2,15 @@ #include "pico/stdlib.h" #include "pico/time.h" +// clang-format off +// Kept as a 4x4 grid mirroring the physical keypad layout. static const char KEY_MAP[KEYPAD_ROWS][KEYPAD_COLS] = { {'1', '2', '3', 'A'}, {'4', '5', '6', 'B'}, {'7', '8', '9', 'C'}, {'*', '0', '#', 'D'} }; +// clang-format on void keypad_init(void) { // Rows: outputs, default HIGH @@ -48,16 +51,16 @@ static char scan_raw(void) { } char keypad_get_key(void) { - static char last_raw = 0; - static char last_stable = 0; - static absolute_time_t stable_at = {0}; + static char last_raw = 0; + static char last_stable = 0; + static absolute_time_t stable_at = {0}; char raw = scan_raw(); if (raw != last_raw) { // Reading changed - restart debounce timer - last_raw = raw; - stable_at = make_timeout_time_ms(KEYPAD_DEBOUNCE_MS); + last_raw = raw; + stable_at = make_timeout_time_ms(KEYPAD_DEBOUNCE_MS); return 0; } diff --git a/hardware/light.c b/hardware/light.c index 23ebe5a..32c565e 100644 --- a/hardware/light.c +++ b/hardware/light.c @@ -3,7 +3,6 @@ #include - void light_init() { gpio_init(LIGHT_PIN); gpio_set_dir(LIGHT_PIN, GPIO_OUT); diff --git a/libs/base32/base32.c b/libs/base32/base32.c index ae245fa..1e3a570 100644 --- a/libs/base32/base32.c +++ b/libs/base32/base32.c @@ -3,8 +3,8 @@ static const char B32_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; void base32_encode(const uint8_t *in, size_t in_len, char *out) { - int buffer = 0; - int bits_left = 0; + uint32_t buffer = 0; + int bits_left = 0; size_t out_len = 0; for (size_t i = 0; i < in_len; i++) { diff --git a/lwipopts.h b/lwipopts.h index 3cc083c..b92fa67 100644 --- a/lwipopts.h +++ b/lwipopts.h @@ -1,92 +1,91 @@ #ifndef _LWIPOPTS_EXAMPLE_COMMONH_H #define _LWIPOPTS_EXAMPLE_COMMONH_H - // Common settings used in most of the pico_w examples // (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details) // allow override in some examples #ifndef NO_SYS -#define NO_SYS 1 +#define NO_SYS 1 #endif // allow override in some examples #ifndef LWIP_SOCKET -#define LWIP_SOCKET 0 +#define LWIP_SOCKET 0 #endif #if PICO_CYW43_ARCH_POLL -#define MEM_LIBC_MALLOC 1 +#define MEM_LIBC_MALLOC 1 #else // MEM_LIBC_MALLOC is incompatible with non polling versions -#define MEM_LIBC_MALLOC 0 +#define MEM_LIBC_MALLOC 0 #endif -#define MEM_ALIGNMENT 4 +#define MEM_ALIGNMENT 4 #ifndef MEM_SIZE -#define MEM_SIZE 4000 +#define MEM_SIZE 4000 #endif -#define MEMP_NUM_TCP_SEG 32 -#define MEMP_NUM_ARP_QUEUE 10 -#define PBUF_POOL_SIZE 24 -#define LWIP_ARP 1 -#define LWIP_ETHERNET 1 -#define LWIP_ICMP 1 -#define LWIP_RAW 1 -#define TCP_WND (8 * TCP_MSS) -#define TCP_MSS 1460 -#define TCP_SND_BUF (8 * TCP_MSS) -#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) -#define LWIP_NETIF_STATUS_CALLBACK 1 -#define LWIP_NETIF_LINK_CALLBACK 1 -#define LWIP_NETIF_HOSTNAME 1 -#define LWIP_NETCONN 0 -#define MEM_STATS 0 -#define SYS_STATS 0 -#define MEMP_STATS 0 -#define LINK_STATS 0 +#define MEMP_NUM_TCP_SEG 32 +#define MEMP_NUM_ARP_QUEUE 10 +#define PBUF_POOL_SIZE 24 +#define LWIP_ARP 1 +#define LWIP_ETHERNET 1 +#define LWIP_ICMP 1 +#define LWIP_RAW 1 +#define TCP_WND (8 * TCP_MSS) +#define TCP_MSS 1460 +#define TCP_SND_BUF (8 * TCP_MSS) +#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_NETIF_HOSTNAME 1 +#define LWIP_NETCONN 0 +#define MEM_STATS 0 +#define SYS_STATS 0 +#define MEMP_STATS 0 +#define LINK_STATS 0 // #define ETH_PAD_SIZE 2 -#define LWIP_CHKSUM_ALGORITHM 3 -#define LWIP_DHCP 1 -#define LWIP_IPV4 1 -#define LWIP_TCP 1 -#define LWIP_UDP 1 -#define LWIP_DNS 1 -#define LWIP_TCP_KEEPALIVE 1 -#define LWIP_NETIF_TX_SINGLE_PBUF 1 -#define DHCP_DOES_ARP_CHECK 0 -#define LWIP_DHCP_DOES_ACD_CHECK 0 +#define LWIP_CHKSUM_ALGORITHM 3 +#define LWIP_DHCP 1 +#define LWIP_IPV4 1 +#define LWIP_TCP 1 +#define LWIP_UDP 1 +#define LWIP_DNS 1 +#define LWIP_TCP_KEEPALIVE 1 +#define LWIP_NETIF_TX_SINGLE_PBUF 1 +#define DHCP_DOES_ARP_CHECK 0 +#define LWIP_DHCP_DOES_ACD_CHECK 0 #ifndef NDEBUG -#define LWIP_DEBUG 1 -#define LWIP_STATS 1 -#define LWIP_STATS_DISPLAY 1 +#define LWIP_DEBUG 1 +#define LWIP_STATS 1 +#define LWIP_STATS_DISPLAY 1 #endif -#define ETHARP_DEBUG LWIP_DBG_OFF -#define NETIF_DEBUG LWIP_DBG_OFF -#define PBUF_DEBUG LWIP_DBG_OFF -#define API_LIB_DEBUG LWIP_DBG_OFF -#define API_MSG_DEBUG LWIP_DBG_OFF -#define SOCKETS_DEBUG LWIP_DBG_OFF -#define ICMP_DEBUG LWIP_DBG_OFF -#define INET_DEBUG LWIP_DBG_OFF -#define IP_DEBUG LWIP_DBG_OFF -#define IP_REASS_DEBUG LWIP_DBG_OFF -#define RAW_DEBUG LWIP_DBG_OFF -#define MEM_DEBUG LWIP_DBG_OFF -#define MEMP_DEBUG LWIP_DBG_OFF -#define SYS_DEBUG LWIP_DBG_OFF -#define TCP_DEBUG LWIP_DBG_OFF -#define TCP_INPUT_DEBUG LWIP_DBG_OFF -#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF -#define TCP_RTO_DEBUG LWIP_DBG_OFF -#define TCP_CWND_DEBUG LWIP_DBG_OFF -#define TCP_WND_DEBUG LWIP_DBG_OFF -#define TCP_FR_DEBUG LWIP_DBG_OFF -#define TCP_QLEN_DEBUG LWIP_DBG_OFF -#define TCP_RST_DEBUG LWIP_DBG_OFF -#define UDP_DEBUG LWIP_DBG_OFF -#define TCPIP_DEBUG LWIP_DBG_OFF -#define PPP_DEBUG LWIP_DBG_OFF -#define SLIP_DEBUG LWIP_DBG_OFF -#define DHCP_DEBUG LWIP_DBG_OFF +#define ETHARP_DEBUG LWIP_DBG_OFF +#define NETIF_DEBUG LWIP_DBG_OFF +#define PBUF_DEBUG LWIP_DBG_OFF +#define API_LIB_DEBUG LWIP_DBG_OFF +#define API_MSG_DEBUG LWIP_DBG_OFF +#define SOCKETS_DEBUG LWIP_DBG_OFF +#define ICMP_DEBUG LWIP_DBG_OFF +#define INET_DEBUG LWIP_DBG_OFF +#define IP_DEBUG LWIP_DBG_OFF +#define IP_REASS_DEBUG LWIP_DBG_OFF +#define RAW_DEBUG LWIP_DBG_OFF +#define MEM_DEBUG LWIP_DBG_OFF +#define MEMP_DEBUG LWIP_DBG_OFF +#define SYS_DEBUG LWIP_DBG_OFF +#define TCP_DEBUG LWIP_DBG_OFF +#define TCP_INPUT_DEBUG LWIP_DBG_OFF +#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF +#define TCP_RTO_DEBUG LWIP_DBG_OFF +#define TCP_CWND_DEBUG LWIP_DBG_OFF +#define TCP_WND_DEBUG LWIP_DBG_OFF +#define TCP_FR_DEBUG LWIP_DBG_OFF +#define TCP_QLEN_DEBUG LWIP_DBG_OFF +#define TCP_RST_DEBUG LWIP_DBG_OFF +#define UDP_DEBUG LWIP_DBG_OFF +#define TCPIP_DEBUG LWIP_DBG_OFF +#define PPP_DEBUG LWIP_DBG_OFF +#define SLIP_DEBUG LWIP_DBG_OFF +#define DHCP_DEBUG LWIP_DBG_OFF #endif /* __LWIPOPTS_H__ */ diff --git a/main.c b/main.c index 778d599..78e2f5c 100644 --- a/main.c +++ b/main.c @@ -19,10 +19,10 @@ static void main1(void) { // allow pausing core 1 while writing to flash multicore_lockout_victim_init(); - multicore_fifo_push_blocking(1); // signal core 0: ready + multicore_fifo_push_blocking(1); // signal core 0: ready keypad_init(); - + while (true) { char key = keypad_get_key(); if (key) { @@ -54,8 +54,7 @@ static void boot_network(void) { // Block until first NTP sync succeeds - beep + retry on failure printf("[main] waiting for NTP sync...\r\n"); while (!ntp_sync()) { - printf("[main] NTP sync failed, retrying in %ds...\r\n", - NTP_RETRY_INTERVAL_S); + printf("[main] NTP sync failed, retrying in %ds...\r\n", NTP_RETRY_INTERVAL_S); buzzer_beep_short(); sleep_ms(NTP_RETRY_INTERVAL_S * 1000); } @@ -65,27 +64,27 @@ static void boot_network(void) { int main(void) { stdio_init_all(); - + buzzer_init(); latch_init(); light_init(); buzzer_beep_short(); - + // Core 1 must be running and ready before any flash writes multicore_launch_core1(main1); - multicore_fifo_pop_blocking(); // wait for core 1 ready signal + multicore_fifo_pop_blocking(); // wait for core 1 ready signal storage_init(); boot_network(); - + // Startup beep - signals boot completed buzzer_beep_short(); buzzer_beep_short(); - + console_init(); - + while (true) { console_task(); ntp_task(); diff --git a/network/ntp.c b/network/ntp.c index d48ebfc..fa28d3a 100644 --- a/network/ntp.c +++ b/network/ntp.c @@ -16,9 +16,9 @@ // Constants // --------------------------------------------------------------------------- -#define NTP_PORT 123 +#define NTP_PORT 123 #define NTP_SERVER "pool.ntp.org" -#define NTP_DELTA 2208988800UL // seconds between 1900 and 1970 epochs +#define NTP_DELTA 2208988800UL // seconds between 1900 and 1970 epochs // --------------------------------------------------------------------------- // State @@ -32,9 +32,9 @@ typedef enum { NTP_STATE_FAILED, } ntp_state_t; -static ntp_state_t ntp_state = NTP_STATE_IDLE; -static struct udp_pcb *ntp_pcb = NULL; -static ip_addr_t server_addr; +static ntp_state_t ntp_state = NTP_STATE_IDLE; +static struct udp_pcb *ntp_pcb = NULL; +static ip_addr_t server_addr; static bool synced = false; static uint32_t last_sync_unix = 0; @@ -45,15 +45,15 @@ static uint64_t last_sync_monotonic_us = 0; // --------------------------------------------------------------------------- static bool rollback_check(uint32_t new_time) { - if (!synced) return true; // no floor before first sync + if (!synced) + return true; // no floor before first sync uint64_t elapsed_us = time_us_64() - last_sync_monotonic_us; uint32_t elapsed_s = (uint32_t)(elapsed_us / 1000000ULL); uint32_t floor = last_sync_unix + elapsed_s - NTP_ROLLBACK_EPSILON_S; if (new_time < floor) { - printf("[ntp] rollback rejected: got %u, floor is %u\r\n", - new_time, floor); + printf("[ntp] rollback rejected: got %u, floor is %u\r\n", new_time, floor); return false; } @@ -76,8 +76,8 @@ static void apply_time(uint32_t unix_time) { // NTP response callback // --------------------------------------------------------------------------- -static void ntp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, - const ip_addr_t *addr, u16_t port) { +static void ntp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, + u16_t port) { if (p->len < 48) { printf("[ntp] response too short\r\n"); pbuf_free(p); @@ -89,11 +89,8 @@ static void ntp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, pbuf_copy_partial(p, buf, 48, 0); pbuf_free(p); - uint32_t seconds_since_1900 = - ((uint32_t)buf[40] << 24) | - ((uint32_t)buf[41] << 16) | - ((uint32_t)buf[42] << 8) | - (uint32_t)buf[43]; + uint32_t seconds_since_1900 = ((uint32_t)buf[40] << 24) | ((uint32_t)buf[41] << 16) | + ((uint32_t)buf[42] << 8) | (uint32_t)buf[43]; uint32_t unix_time = seconds_since_1900 - NTP_DELTA; @@ -119,10 +116,10 @@ static void dns_found_cb(const char *name, const ip_addr_t *ipaddr, void *arg) { server_addr = *ipaddr; - struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, 48, PBUF_RAM); - uint8_t *req = (uint8_t *)p->payload; + struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, 48, PBUF_RAM); + uint8_t *req = (uint8_t *)p->payload; memset(req, 0, 48); - req[0] = 0x1B; // LI=0, VN=3, Mode=3 (client) + req[0] = 0x1B; // LI=0, VN=3, Mode=3 (client) udp_sendto(ntp_pcb, p, &server_addr, NTP_PORT); pbuf_free(p); @@ -180,7 +177,7 @@ bool ntp_sync(void) { } } - bool ok = ntp_state == NTP_STATE_SUCCESS; + bool ok = ntp_state == NTP_STATE_SUCCESS; ntp_state = NTP_STATE_IDLE; udp_remove(ntp_pcb); @@ -190,12 +187,14 @@ bool ntp_sync(void) { } void ntp_task(void) { - if (!synced) return; + if (!synced) + return; uint64_t elapsed_us = time_us_64() - last_sync_monotonic_us; uint32_t elapsed_s = (uint32_t)(elapsed_us / 1000000ULL); - if (elapsed_s < NTP_RESYNC_INTERVAL_S) return; + if (elapsed_s < NTP_RESYNC_INTERVAL_S) + return; printf("[ntp] periodic resync...\r\n"); if (!ntp_sync()) { diff --git a/network/ntp.h b/network/ntp.h index 012e2c4..bdd455d 100644 --- a/network/ntp.h +++ b/network/ntp.h @@ -4,10 +4,10 @@ #include #include -#define NTP_RESYNC_INTERVAL_S (30 * 60) // 30 minutes -#define NTP_RETRY_INTERVAL_S 5 // retry on boot failure -#define NTP_TIMEOUT_S 15 // per-sync timeout -#define NTP_ROLLBACK_EPSILON_S 60 // max allowed backward correction +#define NTP_RESYNC_INTERVAL_S (30 * 60) // 30 minutes +#define NTP_RETRY_INTERVAL_S 5 // retry on boot failure +#define NTP_TIMEOUT_S 15 // per-sync timeout +#define NTP_ROLLBACK_EPSILON_S 60 // max allowed backward correction // Call once after WiFi connected - initialises RTC void ntp_init(void); diff --git a/network/wifi.c b/network/wifi.c index 1998726..7114ccc 100644 --- a/network/wifi.c +++ b/network/wifi.c @@ -15,8 +15,7 @@ bool wifi_connect(const char *ssid, const char *password) { } printf("[wifi] connecting to '%s'...\r\n", ssid); - int rc = cyw43_arch_wifi_connect_timeout_ms( - ssid, password, CYW43_AUTH_WPA2_AES_PSK, 15000); + int rc = cyw43_arch_wifi_connect_timeout_ms(ssid, password, CYW43_AUTH_WPA2_AES_PSK, 15000); if (rc) { printf("[wifi] connect failed: %d\r\n", rc); @@ -28,6 +27,7 @@ bool wifi_connect(const char *ssid, const char *password) { } bool wifi_is_connected(void) { - if (!initialised) return false; + if (!initialised) + return false; return cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA) == CYW43_LINK_UP; } diff --git a/serial/commands.c b/serial/commands.c index 6d88c44..e0d66bb 100644 --- a/serial/commands.c +++ b/serial/commands.c @@ -34,7 +34,7 @@ typedef struct { int max_args; const char *usage; const char *description; - void (*handler)(int argc, char **argv); + void (*handler)(int argc, char **argv); } command_t; // --------------------------------------------------------------------------- @@ -42,28 +42,29 @@ typedef struct { // --------------------------------------------------------------------------- static const command_t COMMANDS[] = { - {"help", false, 0, 0, "help", "Print available commands"}, - {"?", false, 0, 0, "?", "Alias for help"}, - {"status", false, 0, 0, "status", "Show system status"}, - {"test", false, 0, 0, "test", "Test LED, buzzer and latch"}, - {"login", false, 1, 1, "login ", "Enable admin mode via TOTP"}, - {"logout", true, 0, 0, "logout", "End admin session"}, - {"reboot", true, 0, 0, "reboot", "Reboot device"}, - {"get-time", false, 0, 0, "get-time", "Show current RTC time and last NTP sync"}, - {"sync-ntp", true, 0, 0, "sync-ntp", "Force immediate NTP resync"}, - {"set-wifi", true, 2, 2, "set-wifi ", "Save WiFi credentials"}, - {"list-keys", true, 0, 0, "list-keys", "List all keys (no secrets)"}, - {"get-key", true, 1, 1, "get-key ", "Show key details (no secret)"}, - {"get-key-secret", true, 1, 1, "get-key-secret ", "Show secret + QR code for key"}, - {"add-key", true, 2, 2, "add-key ", "Generate and save new key"}, - {"rename-key", true, 2, 2, "rename-key ", "Rename a key"}, - {"enable-key", true, 1, 1, "enable-key ", "Enable a disabled key"}, - {"disable-key", true, 1, 1, "disable-key ", "Disable a key without deleting"}, - {"delete-key", true, 1, 1, "delete-key ", "Permanently delete a key"}, - {"set-key-admin", true, 1, 1, "set-key-admin ", "Grant admin flag to key"}, - {"unset-key-admin", true, 1, 1, "unset-key-admin ", "Remove admin flag from key"}, - {"export-keys", true, 0, 0, "export-keys", "Dump all keys as base64"}, - {"import-keys", true, 1, 1, "import-keys ", "Export backup then overwrite with provided data"}, + {"help", false, 0, 0, "help", "Print available commands"}, + {"?", false, 0, 0, "?", "Alias for help"}, + {"status", false, 0, 0, "status", "Show system status"}, + {"test", false, 0, 0, "test", "Test LED, buzzer and latch"}, + {"login", false, 1, 1, "login ", "Enable admin mode via TOTP"}, + {"logout", true, 0, 0, "logout", "End admin session"}, + {"reboot", true, 0, 0, "reboot", "Reboot device"}, + {"get-time", false, 0, 0, "get-time", "Show current RTC time and last NTP sync"}, + {"sync-ntp", true, 0, 0, "sync-ntp", "Force immediate NTP resync"}, + {"set-wifi", true, 2, 2, "set-wifi ", "Save WiFi credentials"}, + {"list-keys", true, 0, 0, "list-keys", "List all keys (no secrets)"}, + {"get-key", true, 1, 1, "get-key ", "Show key details (no secret)"}, + {"get-key-secret", true, 1, 1, "get-key-secret ", "Show secret + QR code for key"}, + {"add-key", true, 2, 2, "add-key ", "Generate and save new key"}, + {"rename-key", true, 2, 2, "rename-key ", "Rename a key"}, + {"enable-key", true, 1, 1, "enable-key ", "Enable a disabled key"}, + {"disable-key", true, 1, 1, "disable-key ", "Disable a key without deleting"}, + {"delete-key", true, 1, 1, "delete-key ", "Permanently delete a key"}, + {"set-key-admin", true, 1, 1, "set-key-admin ", "Grant admin flag to key"}, + {"unset-key-admin", true, 1, 1, "unset-key-admin ", "Remove admin flag from key"}, + {"export-keys", true, 0, 0, "export-keys", "Dump all keys as base64"}, + {"import-keys", true, 1, 1, "import-keys ", + "Export backup then overwrite with provided data"}, }; #define NUM_COMMANDS (sizeof(COMMANDS) / sizeof(COMMANDS[0])) @@ -75,13 +76,14 @@ static const command_t COMMANDS[] = { static void cmd_help(int argc, char **argv) { printf("\r\navailable commands:\r\n\r\n"); printf(" %-32s %s\r\n", "usage", "description"); - printf(" %-32s %s\r\n", - "--------------------------------", + printf(" %-32s %s\r\n", "--------------------------------", "-----------------------------------"); for (size_t i = 0; i < NUM_COMMANDS; i++) { const command_t *cmd = &COMMANDS[i]; - if (cmd->requires_admin && !admin_mode) continue; - if (strcmp(cmd->name, "?") == 0) continue; + if (cmd->requires_admin && !admin_mode) + continue; + if (strcmp(cmd->name, "?") == 0) + continue; printf(" %-32s %s\r\n", cmd->usage, cmd->description); } printf("\r\n"); @@ -89,29 +91,12 @@ static void cmd_help(int argc, char **argv) { } // Handler lookup - must match COMMANDS table order -static void (*const HANDLERS[])(int, char**) = { - cmd_help, - cmd_help, - cmd_status, - cmd_test, - cmd_login, - cmd_logout, - cmd_reboot, - cmd_get_time, - cmd_sync_ntp, - cmd_set_wifi, - cmd_list_keys, - cmd_get_key, - cmd_get_key_secret, - cmd_add_key, - cmd_rename_key, - cmd_enable_key, - cmd_disable_key, - cmd_delete_key, - cmd_set_key_admin, - cmd_unset_key_admin, - cmd_export_keys, - cmd_import_keys, +static void (*const HANDLERS[])(int, char **) = { + cmd_help, cmd_help, cmd_status, cmd_test, cmd_login, + cmd_logout, cmd_reboot, cmd_get_time, cmd_sync_ntp, cmd_set_wifi, + cmd_list_keys, cmd_get_key, cmd_get_key_secret, cmd_add_key, cmd_rename_key, + cmd_enable_key, cmd_disable_key, cmd_delete_key, cmd_set_key_admin, cmd_unset_key_admin, + cmd_export_keys, cmd_import_keys, }; // --------------------------------------------------------------------------- @@ -120,13 +105,13 @@ static void (*const HANDLERS[])(int, char**) = { void commands_dispatch(int argc, char **argv) { for (size_t i = 0; i < NUM_COMMANDS; i++) { - if (strcmp(argv[0], COMMANDS[i].name) != 0) continue; + if (strcmp(argv[0], COMMANDS[i].name) != 0) + continue; const command_t *cmd = &COMMANDS[i]; if (cmd->requires_admin && !admin_mode) { - printf("error: '%s' requires admin mode - use login \r\n", - cmd->name); + printf("error: '%s' requires admin mode - use login \r\n", cmd->name); buzzer_play_command_ack(); return; } @@ -142,7 +127,6 @@ void commands_dispatch(int argc, char **argv) { return; } - printf("error: unknown command '%s' - type 'help' for available commands\r\n", - argv[0]); + printf("error: unknown command '%s' - type 'help' for available commands\r\n", argv[0]); buzzer_play_command_ack(); } diff --git a/serial/commands_keys.c b/serial/commands_keys.c index c216200..f158349 100644 --- a/serial/commands_keys.c +++ b/serial/commands_keys.c @@ -13,7 +13,7 @@ void cmd_list_keys(int argc, char **argv) { key_record_t keys[BACKUP_MAX_KEYS]; - int count = storage_key_list(keys, BACKUP_MAX_KEYS); + int count = storage_key_list(keys, BACKUP_MAX_KEYS); if (count < 0) { printf("error: failed to read keys\r\n"); @@ -26,8 +26,8 @@ void cmd_list_keys(int argc, char **argv) { return; } - printf("%-6s %-2s %-24s %-7s %-5s %s\r\n", - "id", "ok", "name", "enabled", "admin", "created"); + printf("%-6s %-2s %-24s %-7s %-5s %s\r\n", "id", "ok", "name", "enabled", "admin", + "created"); printf("------ -- ------------------------ ------- ----- -------------------\r\n"); for (int i = 0; i < count; i++) { @@ -35,18 +35,13 @@ void cmd_list_keys(int argc, char **argv) { char created[20] = "unknown"; if (k->created_at > 0) { - time_t t = (time_t)k->created_at; + time_t t = (time_t)k->created_at; struct tm *tm = gmtime(&t); strftime(created, sizeof(created), "%Y-%m-%d %H:%M:%S", tm); } - printf("%-6u %-2s %-24s %-7s %-5s %s\r\n", - k->id, - k->is_checksum_valid ? "v" : "x", - k->name, - k->is_enabled ? "yes" : "no", - k->is_admin ? "yes" : "no", - created); + printf("%-6u %-2s %-24s %-7s %-5s %s\r\n", k->id, k->is_checksum_valid ? "v" : "x", + k->name, k->is_enabled ? "yes" : "no", k->is_admin ? "yes" : "no", created); } printf("\r\n%d key(s)\r\n", count); @@ -71,17 +66,17 @@ void cmd_get_key(int argc, char **argv) { char created[20] = "unknown"; if (key.created_at > 0) { - time_t t = (time_t)key.created_at; + time_t t = (time_t)key.created_at; struct tm *tm = gmtime(&t); strftime(created, sizeof(created), "%Y-%m-%d %H:%M:%S", tm); } - printf("id: %u\r\n", key.id); - printf("ok: %s\r\n", key.is_checksum_valid ? "v" : "x"); - printf("name: %s\r\n", key.name); - printf("enabled: %s\r\n", key.is_enabled ? "yes" : "no"); - printf("admin: %s\r\n", key.is_admin ? "yes" : "no"); - printf("created: %s\r\n", created); + printf("id: %u\r\n", key.id); + printf("ok: %s\r\n", key.is_checksum_valid ? "v" : "x"); + printf("name: %s\r\n", key.name); + printf("enabled: %s\r\n", key.is_enabled ? "yes" : "no"); + printf("admin: %s\r\n", key.is_admin ? "yes" : "no"); + printf("created: %s\r\n", created); printf("secret: "); for (int i = 0; i < KEY_SECRET_LEN; i++) { printf("%02X", key.secret[i]); @@ -92,9 +87,9 @@ void cmd_get_key(int argc, char **argv) { } // Łódź URL-encoded: Ł=%C5%81, ó=%C3%B3, ź=%C5%BA, space=%20 -#define ISSUER_ENC "Hackerspejs%20%C5%81%C3%B3d%C5%BA" -#define QR_BORDER 2 // quiet zone — required for scanners to work -#define SECRET_B32_LEN BASE32_ENCODED_LEN(KEY_SECRET_LEN) // 33 bytes for 20-byte secret +#define ISSUER_ENC "Hackerspejs%20%C5%81%C3%B3d%C5%BA" +#define QR_BORDER 2 // quiet zone — required for scanners to work +#define SECRET_B32_LEN BASE32_ENCODED_LEN(KEY_SECRET_LEN) // 33 bytes for 20-byte secret void cmd_get_key_secret(int argc, char **argv) { uint16_t id = (uint16_t)strtoul(argv[1], NULL, 10); @@ -124,9 +119,8 @@ void cmd_get_key_secret(int argc, char **argv) { // Build otpauth URI char uri[200]; - snprintf(uri, sizeof(uri), - "otpauth://totp/%s:key%%20%u?secret=%s&issuer=%s&digits=6&period=30", - ISSUER_ENC, id, secret_b32, ISSUER_ENC); + snprintf(uri, sizeof(uri), "otpauth://totp/%s:key%%20%u?secret=%s&issuer=%s&digits=6&period=30", + ISSUER_ENC, id, secret_b32, ISSUER_ENC); printf("secret: %s\r\n", secret_b32); printf("uri: %s\r\n\r\n", uri); @@ -135,12 +129,8 @@ void cmd_get_key_secret(int argc, char **argv) { static uint8_t qr[qrcodegen_BUFFER_LEN_MAX]; static uint8_t tmp[qrcodegen_BUFFER_LEN_MAX]; - bool ok = qrcodegen_encodeText(uri, tmp, qr, - qrcodegen_Ecc_MEDIUM, - qrcodegen_VERSION_MIN, - qrcodegen_VERSION_MAX, - qrcodegen_Mask_AUTO, - true); + bool ok = qrcodegen_encodeText(uri, tmp, qr, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); if (!ok) { printf("error: QR generation failed\r\n"); buzzer_play_command_ack(); @@ -151,8 +141,7 @@ void cmd_get_key_secret(int argc, char **argv) { int size = qrcodegen_getSize(qr); for (int y = -QR_BORDER; y < size + QR_BORDER; y++) { for (int x = -QR_BORDER; x < size + QR_BORDER; x++) { - bool dark = (x >= 0 && y >= 0 && x < size && y < size) - && qrcodegen_getModule(qr, x, y); + bool dark = (x >= 0 && y >= 0 && x < size && y < size) && qrcodegen_getModule(qr, x, y); printf(dark ? "##" : " "); } printf("\r\n"); @@ -183,12 +172,12 @@ void cmd_add_key(int argc, char **argv) { return; } - key_record_t key = {0}; - key.id = id; - key.is_enabled = true; - key.is_admin = false; - key.is_checksum_valid = true; - key.created_at = clock_get_unix_time(); + key_record_t key = {0}; + key.id = id; + key.is_enabled = true; + key.is_admin = false; + key.is_checksum_valid = true; + key.created_at = clock_get_unix_time(); strncpy(key.name, argv[2], KEY_NAME_MAX - 1); generate_secret(key.secret); diff --git a/serial/commands_network.c b/serial/commands_network.c index 76f96d6..15f2afc 100644 --- a/serial/commands_network.c +++ b/serial/commands_network.c @@ -31,7 +31,7 @@ void cmd_set_wifi(int argc, char **argv) { return; } - strncpy(cfg.ssid, argv[1], WIFI_SSID_MAX - 1); + strncpy(cfg.ssid, argv[1], WIFI_SSID_MAX - 1); strncpy(cfg.password, argv[2], WIFI_PASSWORD_MAX - 1); cfg.ssid[WIFI_SSID_MAX - 1] = '\0'; cfg.password[WIFI_PASSWORD_MAX - 1] = '\0'; diff --git a/serial/commands_system.c b/serial/commands_system.c index 36f7141..5a1dfc9 100644 --- a/serial/commands_system.c +++ b/serial/commands_system.c @@ -24,9 +24,9 @@ void cmd_status(int argc, char **argv) { uint32_t last = ntp_last_sync_time(); if (last > 0) { - time_t lt = (time_t)last; + time_t lt = (time_t)last; struct tm *ltm = gmtime(<); - char lbuf[20]; + char lbuf[20]; strftime(lbuf, sizeof(lbuf), "%Y-%m-%d %H:%M:%S", ltm); printf("last sync: %s UTC\r\n", lbuf); } else { @@ -45,17 +45,17 @@ void cmd_get_time(int argc, char **argv) { return; } - time_t t = (time_t)unix_time; + time_t t = (time_t)unix_time; struct tm *tm = gmtime(&t); - char buf[20]; + char buf[20]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm); printf("time: %s UTC\r\n", buf); uint32_t last = ntp_last_sync_time(); if (last > 0) { - time_t lt = (time_t)last; + time_t lt = (time_t)last; struct tm *ltm = gmtime(<); - char lbuf[20]; + char lbuf[20]; strftime(lbuf, sizeof(lbuf), "%Y-%m-%d %H:%M:%S", ltm); printf("last sync: %s UTC\r\n", lbuf); } else { @@ -93,5 +93,6 @@ void cmd_reboot(int argc, char **argv) { printf("rebooting...\r\n"); buzzer_play_command_ack(); watchdog_reboot(0, 0, 100); - while (true) tight_loop_contents(); + while (true) + tight_loop_contents(); } diff --git a/serial/console.c b/serial/console.c index 5db95c4..642e786 100644 --- a/serial/console.c +++ b/serial/console.c @@ -9,7 +9,7 @@ #define MAX_ARGS 8 static char input_buf[INPUT_BUF_SIZE]; -static int input_len = 0; +static int input_len = 0; static bool was_connected = false; static void print_prompt(void) { @@ -27,14 +27,19 @@ static void process_line(void) { char *p = input_buf; while (*p && argc < MAX_ARGS) { - while (*p == ' ') p++; // skip whitespace - if (!*p) break; + while (*p == ' ') + p++; // skip whitespace + if (!*p) + break; argv[argc++] = p; - while (*p && *p != ' ') p++; // find end of token - if (*p) *p++ = '\0'; + while (*p && *p != ' ') + p++; // find end of token + if (*p) + *p++ = '\0'; } - if (argc == 0) return; + if (argc == 0) + return; commands_dispatch(argc, argv); } @@ -65,10 +70,12 @@ void console_task(void) { return; } - if (!connected) return; + if (!connected) + return; int c = getchar_timeout_us(0); - if (c == PICO_ERROR_TIMEOUT) return; + if (c == PICO_ERROR_TIMEOUT) + return; if (c == '\r' || c == '\n') { printf("\r\n"); diff --git a/shared/random.c b/shared/random.c index 48ef49c..c6c19de 100644 --- a/shared/random.c +++ b/shared/random.c @@ -5,9 +5,9 @@ void generate_secret(uint8_t *out) { // 20 bytes = 2.5 x uint64_t - fill in 64-bit chunks uint64_t r0 = get_rand_64(); uint64_t r1 = get_rand_64(); - uint64_t r2 = get_rand_64(); // only 4 bytes used from this one + uint64_t r2 = get_rand_64(); // only 4 bytes used from this one - memcpy(out, &r0, 8); - memcpy(out + 8, &r1, 8); + memcpy(out, &r0, 8); + memcpy(out + 8, &r1, 8); memcpy(out + 16, &r2, 4); } \ No newline at end of file diff --git a/storage/backup.c b/storage/backup.c index 04dc1a3..2842eb7 100644 --- a/storage/backup.c +++ b/storage/backup.c @@ -11,7 +11,7 @@ static uint32_t backup_checksum(const backup_key_t *keys, uint32_t count) { uint32_t crc = 0xFFFFFFFF; - crc = lfs_crc(crc, keys, count * sizeof(backup_key_t)); + crc = lfs_crc(crc, keys, count * sizeof(backup_key_t)); return crc; } @@ -25,7 +25,7 @@ static backup_key_t to_backup_key(const key_record_t *k) { b.is_enabled = k->is_enabled; b.is_admin = k->is_admin; b.created_at = k->created_at; - memcpy(b.name, k->name, sizeof(b.name)); + memcpy(b.name, k->name, sizeof(b.name)); memcpy(b.secret, k->secret, sizeof(b.secret)); return b; } @@ -36,37 +36,36 @@ static backup_key_t to_backup_key(const key_record_t *k) { static key_record_t to_key_record(const backup_key_t *b) { key_record_t k; - k.id = b->id; - k.is_enabled = b->is_enabled; - k.is_admin = b->is_admin; - k.created_at = b->created_at; - k.is_checksum_valid = true; - memcpy(k.name, b->name, sizeof(k.name)); + k.id = b->id; + k.is_enabled = b->is_enabled; + k.is_admin = b->is_admin; + k.created_at = b->created_at; + k.is_checksum_valid = true; + memcpy(k.name, b->name, sizeof(k.name)); memcpy(k.secret, b->secret, sizeof(k.secret)); return k; } - // --------------------------------------------------------------------------- // Export // --------------------------------------------------------------------------- int backup_export(uint8_t *buf, size_t buf_size) { static key_record_t records[BACKUP_MAX_KEYS]; - int count = storage_key_list(records, BACKUP_MAX_KEYS); - if (count < 0) return -1; + int count = storage_key_list(records, BACKUP_MAX_KEYS); + if (count < 0) + return -1; - size_t needed = sizeof(backup_header_t) - + count * sizeof(backup_key_t); - if (buf_size < needed) return -1; + size_t needed = sizeof(backup_header_t) + count * sizeof(backup_key_t); + if (buf_size < needed) + return -1; // Populate backup keys - backup_key_t *keys = (backup_key_t *)(buf + sizeof(backup_header_t)); - int exported_key_count = 0; + backup_key_t *keys = (backup_key_t *)(buf + sizeof(backup_header_t)); + int exported_key_count = 0; for (int i = 0; i < count; i++) { if (!records[i].is_checksum_valid) { - printf("[backup] export: key %u has invalid checksum, skipping\r\n", - records[i].id); + printf("[backup] export: key %u has invalid checksum, skipping\r\n", records[i].id); continue; } keys[exported_key_count++] = to_backup_key(&records[i]); @@ -109,15 +108,13 @@ bool backup_import(const uint8_t *buf, size_t size) { return false; } - size_t expected = sizeof(backup_header_t) - + hdr->key_count * sizeof(backup_key_t); + size_t expected = sizeof(backup_header_t) + hdr->key_count * sizeof(backup_key_t); if (size < expected) { printf("[backup] import: truncated data\r\n"); return false; } - const backup_key_t *keys = - (const backup_key_t *)(buf + sizeof(backup_header_t)); + const backup_key_t *keys = (const backup_key_t *)(buf + sizeof(backup_header_t)); // Verify whole-backup checksum uint32_t expected_crc = backup_checksum(keys, hdr->key_count); @@ -128,7 +125,7 @@ bool backup_import(const uint8_t *buf, size_t size) { // Checksum valid - delete existing keys static key_record_t existing[BACKUP_MAX_KEYS]; - int existing_count = storage_key_list(existing, BACKUP_MAX_KEYS); + int existing_count = storage_key_list(existing, BACKUP_MAX_KEYS); for (int i = 0; i < existing_count; i++) { storage_key_delete(existing[i].id); } diff --git a/storage/backup.h b/storage/backup.h index a49718c..afae954 100644 --- a/storage/backup.h +++ b/storage/backup.h @@ -10,7 +10,7 @@ // Binary format (base64-encoded for serial transport) // --------------------------------------------------------------------------- -#define BACKUP_MAGIC 0x4C4C5348U // "HSLL" +#define BACKUP_MAGIC 0x4C4C5348U // "HSLL" #define BACKUP_VERSION 1 #define BACKUP_MAX_KEYS KEY_MAX_COUNT @@ -18,7 +18,7 @@ typedef struct __attribute__((packed)) { uint32_t magic; uint32_t version; uint32_t key_count; - uint32_t checksum; // covers all backup_key_t records + uint32_t checksum; // covers all backup_key_t records } backup_header_t; typedef struct __attribute__((packed)) { @@ -35,7 +35,7 @@ typedef struct __attribute__((packed)) { // --------------------------------------------------------------------------- // Serialise all keys into buf. Returns byte count written, -1 on error. -int backup_export(uint8_t *buf, size_t buf_size); +int backup_export(uint8_t *buf, size_t buf_size); // Overwrite all keys from buf. Returns false on error. bool backup_import(const uint8_t *buf, size_t size); diff --git a/storage/storage.c b/storage/storage.c index de14316..950125c 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -18,16 +18,16 @@ // The linker places firmware at the START of flash; storage is at the END, // so they never collide as long as firmware stays under ~1.75MB. -#define STORAGE_SIZE_BYTES (256 * 1024) +#define STORAGE_SIZE_BYTES (256 * 1024) #define STORAGE_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - STORAGE_SIZE_BYTES) // LittleFS block = one flash erase sector -#define LFS_BLOCK_SIZE FLASH_SECTOR_SIZE // 4096 -#define LFS_BLOCK_COUNT (STORAGE_SIZE_BYTES / LFS_BLOCK_SIZE) // 64 -#define LFS_READ_SIZE FLASH_PAGE_SIZE // 256 -#define LFS_PROG_SIZE FLASH_PAGE_SIZE // 256 -#define LFS_CACHE_SIZE FLASH_PAGE_SIZE // 256 -#define LFS_LOOKAHEAD 64 +#define LFS_BLOCK_SIZE FLASH_SECTOR_SIZE // 4096 +#define LFS_BLOCK_COUNT (STORAGE_SIZE_BYTES / LFS_BLOCK_SIZE) // 64 +#define LFS_READ_SIZE FLASH_PAGE_SIZE // 256 +#define LFS_PROG_SIZE FLASH_PAGE_SIZE // 256 +#define LFS_CACHE_SIZE FLASH_PAGE_SIZE // 256 +#define LFS_LOOKAHEAD 64 static uint8_t lfs_file_buf[LFS_CACHE_SIZE]; @@ -51,12 +51,12 @@ typedef struct { static uint32_t key_checksum(const key_record_stored_t *key) { uint32_t crc = 0xFFFFFFFF; - crc = lfs_crc(crc, &key->id, sizeof(key->id)); - crc = lfs_crc(crc, key->name, sizeof(key->name)); - crc = lfs_crc(crc, key->secret, sizeof(key->secret)); - crc = lfs_crc(crc, &key->is_enabled, sizeof(key->is_enabled)); - crc = lfs_crc(crc, &key->is_admin, sizeof(key->is_admin)); - crc = lfs_crc(crc, &key->created_at, sizeof(key->created_at)); + crc = lfs_crc(crc, &key->id, sizeof(key->id)); + crc = lfs_crc(crc, key->name, sizeof(key->name)); + crc = lfs_crc(crc, key->secret, sizeof(key->secret)); + crc = lfs_crc(crc, &key->is_enabled, sizeof(key->is_enabled)); + crc = lfs_crc(crc, &key->is_admin, sizeof(key->is_admin)); + crc = lfs_crc(crc, &key->created_at, sizeof(key->created_at)); return crc; } @@ -66,20 +66,20 @@ static key_record_stored_t to_stored(const key_record_t *k) { s.is_enabled = k->is_enabled; s.is_admin = k->is_admin; s.created_at = k->created_at; - memcpy(s.name, k->name, sizeof(s.name)); + memcpy(s.name, k->name, sizeof(s.name)); memcpy(s.secret, k->secret, sizeof(s.secret)); - s.checksum = key_checksum(&s); + s.checksum = key_checksum(&s); return s; } static key_record_t to_record(const key_record_stored_t *s) { key_record_t k; - k.id = s->id; - k.is_enabled = s->is_enabled; - k.is_admin = s->is_admin; - k.created_at = s->created_at; - k.is_checksum_valid = (s->checksum == key_checksum(s)); - memcpy(k.name, s->name, sizeof(k.name)); + k.id = s->id; + k.is_enabled = s->is_enabled; + k.is_admin = s->is_admin; + k.created_at = s->created_at; + k.is_checksum_valid = (s->checksum == key_checksum(s)); + memcpy(k.name, s->name, sizeof(k.name)); memcpy(k.secret, s->secret, sizeof(k.secret)); return k; } @@ -88,17 +88,23 @@ static key_record_t to_record(const key_record_stored_t *s) { // Flash block device callbacks // --------------------------------------------------------------------------- -static int flash_read(const struct lfs_config *c, lfs_block_t block, - lfs_off_t off, void *buffer, lfs_size_t size) { - uint32_t addr = XIP_BASE + STORAGE_FLASH_OFFSET - + block * LFS_BLOCK_SIZE + off; +static int flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, + lfs_size_t size) { + uint32_t addr = XIP_BASE + STORAGE_FLASH_OFFSET + block * LFS_BLOCK_SIZE + off; memcpy(buffer, (const void *)addr, size); return LFS_ERR_OK; } // Params struct for flash_safe_execute callbacks (can't pass multiple args) -typedef struct { uint32_t offset; const uint8_t *data; size_t size; } prog_params_t; -typedef struct { uint32_t offset; size_t size; } erase_params_t; +typedef struct { + uint32_t offset; + const uint8_t *data; + size_t size; +} prog_params_t; +typedef struct { + uint32_t offset; + size_t size; +} erase_params_t; // those functions must live in ram so they can be executed while flash is locked static void __no_inline_not_in_flash_func(do_flash_program)(void *param) { @@ -111,37 +117,33 @@ static void __no_inline_not_in_flash_func(do_flash_erase)(void *param) { flash_range_erase(p->offset, p->size); } -static int flash_prog(const struct lfs_config *c, lfs_block_t block, - lfs_off_t off, const void *buffer, lfs_size_t size) { - prog_params_t p = { - .offset = STORAGE_FLASH_OFFSET + block * LFS_BLOCK_SIZE + off, - .data = (const uint8_t *)buffer, - .size = size - }; +static int flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, + const void *buffer, lfs_size_t size) { + prog_params_t p = {.offset = STORAGE_FLASH_OFFSET + block * LFS_BLOCK_SIZE + off, + .data = (const uint8_t *)buffer, + .size = size}; // flash_safe_execute pauses core 1 and disables interrupts while writing int rc = flash_safe_execute(do_flash_program, &p, UINT32_MAX); return rc == PICO_OK ? LFS_ERR_OK : LFS_ERR_IO; } static int flash_erase(const struct lfs_config *c, lfs_block_t block) { - erase_params_t p = { - .offset = STORAGE_FLASH_OFFSET + block * LFS_BLOCK_SIZE, - .size = LFS_BLOCK_SIZE - }; - int rc = flash_safe_execute(do_flash_erase, &p, UINT32_MAX); + erase_params_t p = {.offset = STORAGE_FLASH_OFFSET + block * LFS_BLOCK_SIZE, + .size = LFS_BLOCK_SIZE}; + int rc = flash_safe_execute(do_flash_erase, &p, UINT32_MAX); return rc == PICO_OK ? LFS_ERR_OK : LFS_ERR_IO; } static int flash_sync(const struct lfs_config *c) { - return LFS_ERR_OK; // no write buffer on NOR flash + return LFS_ERR_OK; // no write buffer on NOR flash } // --------------------------------------------------------------------------- // LittleFS state // --------------------------------------------------------------------------- -static uint8_t lfs_read_buf [LFS_CACHE_SIZE]; -static uint8_t lfs_prog_buf [LFS_CACHE_SIZE]; +static uint8_t lfs_read_buf[LFS_CACHE_SIZE]; +static uint8_t lfs_prog_buf[LFS_CACHE_SIZE]; static uint8_t lfs_lookahead_buf[LFS_LOOKAHEAD / 8]; static const struct lfs_config LFS_CFG = { @@ -156,7 +158,7 @@ static const struct lfs_config LFS_CFG = { .block_count = LFS_BLOCK_COUNT, .cache_size = LFS_CACHE_SIZE, .lookahead_size = LFS_LOOKAHEAD, - .block_cycles = 500, // wear leveling hint + .block_cycles = 500, // wear leveling hint .read_buffer = lfs_read_buf, .prog_buffer = lfs_prog_buf, @@ -195,10 +197,12 @@ bool storage_init(void) { if (rc < 0) { printf("[storage] mount failed, formatting...\r\n"); rc = lfs_format(&lfs, &LFS_CFG); - if (rc < 0) return false; + if (rc < 0) + return false; rc = lfs_mount(&lfs, &LFS_CFG); - if (rc < 0) return false; + if (rc < 0) + return false; } if (!ensure_dirs()) { @@ -216,7 +220,8 @@ bool storage_init(void) { // --------------------------------------------------------------------------- bool storage_wifi_get(wifi_config_t *out) { - if (!mounted) return false; + if (!mounted) + return false; lfs_file_t f; if (lfs_file_opencfg(&lfs, &f, FILE_WIFI, LFS_O_RDONLY, &LFS_FILE_CFG) < 0) @@ -228,12 +233,14 @@ bool storage_wifi_get(wifi_config_t *out) { } bool storage_wifi_set(const wifi_config_t *cfg) { - if (!mounted) return false; + if (!mounted) + return false; lfs_file_t f; - int flags = LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC; - int rc = lfs_file_opencfg(&lfs, &f, FILE_WIFI, flags, &LFS_FILE_CFG); - if (rc < 0) return false; + int flags = LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC; + int rc = lfs_file_opencfg(&lfs, &f, FILE_WIFI, flags, &LFS_FILE_CFG); + if (rc < 0) + return false; lfs_ssize_t n = lfs_file_write(&lfs, &f, cfg, sizeof(wifi_config_t)); lfs_file_close(&lfs, &f); @@ -241,7 +248,8 @@ bool storage_wifi_set(const wifi_config_t *cfg) { } bool storage_wifi_clear(void) { - if (!mounted) return false; + if (!mounted) + return false; return lfs_remove(&lfs, FILE_WIFI) >= 0; } @@ -250,7 +258,8 @@ bool storage_wifi_clear(void) { // --------------------------------------------------------------------------- bool storage_key_exists(uint16_t id) { - if (!mounted) return false; + if (!mounted) + return false; char path[40]; key_path(id, path, sizeof(path)); struct lfs_info info; @@ -258,7 +267,8 @@ bool storage_key_exists(uint16_t id) { } bool storage_key_get(uint16_t id, key_record_t *out) { - if (!mounted) return false; + if (!mounted) + return false; char path[40]; key_path(id, path, sizeof(path)); @@ -267,9 +277,10 @@ bool storage_key_get(uint16_t id, key_record_t *out) { return false; key_record_stored_t stored; - lfs_ssize_t n = lfs_file_read(&lfs, &f, &stored, sizeof(stored)); + lfs_ssize_t n = lfs_file_read(&lfs, &f, &stored, sizeof(stored)); lfs_file_close(&lfs, &f); - if (n != (lfs_ssize_t)sizeof(stored)) return false; + if (n != (lfs_ssize_t)sizeof(stored)) + return false; *out = to_record(&stored); @@ -280,14 +291,15 @@ bool storage_key_get(uint16_t id, key_record_t *out) { } bool storage_key_save(const key_record_t *key) { - if (!mounted) return false; + if (!mounted) + return false; char path[40]; key_path(key->id, path, sizeof(path)); - key_record_stored_t stored = to_stored(key); // checksum computed here + key_record_stored_t stored = to_stored(key); // checksum computed here lfs_file_t f; - int flags = LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC; + int flags = LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC; if (lfs_file_opencfg(&lfs, &f, path, flags, &LFS_FILE_CFG) < 0) return false; @@ -297,28 +309,33 @@ bool storage_key_save(const key_record_t *key) { } bool storage_key_delete(uint16_t id) { - if (!mounted) return false; + if (!mounted) + return false; char path[40]; key_path(id, path, sizeof(path)); return lfs_remove(&lfs, path) >= 0; } int storage_key_list(key_record_t *out, int max_count) { - if (!mounted) return -1; + if (!mounted) + return -1; - if (max_count > KEY_MAX_COUNT) max_count = KEY_MAX_COUNT; + if (max_count > KEY_MAX_COUNT) + max_count = KEY_MAX_COUNT; lfs_dir_t dir; if (lfs_dir_open(&lfs, &dir, DIR_KEYS) < 0) return -1; - int count = 0; + int count = 0; struct lfs_info info; while (count < max_count && lfs_dir_read(&lfs, &dir, &info) > 0) { - if (info.type != LFS_TYPE_REG) continue; + if (info.type != LFS_TYPE_REG) + continue; uint16_t id = (uint16_t)strtoul(info.name, NULL, 10); - if (!storage_key_get(id, &out[count])) continue; + if (!storage_key_get(id, &out[count])) + continue; count++; } diff --git a/storage/storage.h b/storage/storage.h index 88629eb..b0ca965 100644 --- a/storage/storage.h +++ b/storage/storage.h @@ -10,10 +10,10 @@ // --------------------------------------------------------------------------- #define KEY_MAX_COUNT 256 -#define KEY_ID_MAX KEY_MAX_COUNT - 1 +#define KEY_ID_MAX KEY_MAX_COUNT - 1 #define KEY_NAME_MAX 32 -#define KEY_SECRET_LEN 20 // HMAC-SHA1 seed, matches old Arduino format +#define KEY_SECRET_LEN 20 // HMAC-SHA1 seed, matches old Arduino format typedef struct { uint16_t id; @@ -21,7 +21,7 @@ typedef struct { uint8_t secret[KEY_SECRET_LEN]; bool is_enabled; bool is_admin; - uint32_t created_at; // unix timestamp + uint32_t created_at; // unix timestamp bool is_checksum_valid; } key_record_t; @@ -53,10 +53,10 @@ bool storage_wifi_clear(void); // Key CRUD bool storage_key_exists(uint16_t id); bool storage_key_get(uint16_t id, key_record_t *out); -bool storage_key_save(const key_record_t *key); // create or update +bool storage_key_save(const key_record_t *key); // create or update bool storage_key_delete(uint16_t id); // List all keys. Returns count written into out[]. -int storage_key_list(key_record_t *out, int max_count); +int storage_key_list(key_record_t *out, int max_count); #endif \ No newline at end of file diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..0e567ae --- /dev/null +++ b/test/Makefile @@ -0,0 +1,137 @@ +# Host test suite for hslock. +# +# The firmware is cross-compiled for the RP2040 and cannot run natively, but +# the hardware-independent logic can be built as a native Linux ELF and tested. +# This Makefile builds each harness against the REAL first-party sources plus +# cheap host stubs (test/stub/), and runs them under three modes: +# +# make asan ASan + UBSan, aborts on any error (CI gate) +# make valgrind plain build under valgrind, fails on error/leak +# make coverage --coverage build of the harness-exercised modules, run, +# lcov + genhtml report (branch coverage) into coverage/html/ +# make clean +# +# Invoke from the repo root as `make -C test `. + +# Repo layout, relative to this Makefile (test/). +ROOT := .. +BUILD := build +COV_DIR := coverage + +# Absolute repo root, used to filter coverage to first-party sources WITHOUT +# assuming the checkout dir is literally named "hslock". Coverage is built with +# -fprofile-abs-path, so the .info tracefiles carry absolute source paths; +# extracting "$(REPO_ROOT)/*" keeps first-party files regardless of dir name. +# Prefer git's toplevel; fall back to the parent dir for non-git checkouts. +REPO_ROOT := $(or $(shell git -C $(CURDIR) rev-parse --show-toplevel 2>/dev/null),$(abspath $(ROOT))) + +# Real first-party / third-party sources exercised by the harnesses. +SHARED := $(ROOT)/shared +BASE32 := $(ROOT)/libs/base32 +BASE64 := $(ROOT)/libs/base64 +QRCODEGEN := $(ROOT)/libs/qrcodegen/c + +# Include roots. `-I..` resolves module-qualified includes ("hardware/buzzer.h", +# "storage/storage.h", ...); the per-module dirs resolve bare includes +# ("buzzer.h", "base32.h", ...); `-Istub` supplies host shims for Pico-SDK +# headers; `-I$(ROOT)/libs/littlefs` supplies the real lfs_util.h (lfs_crc). +INCLUDES := -Istub -I$(ROOT) \ + -I$(ROOT)/hardware -I$(ROOT)/network -I$(ROOT)/serial \ + -I$(ROOT)/storage -I$(SHARED) -I$(BASE32) -I$(BASE64) \ + -I$(QRCODEGEN) -I$(ROOT)/libs/littlefs + +# Per-harness source lists (harness + its real dependencies). +SECRET_QR_SRCS := harness_secret_qr.c $(SHARED)/random.c $(BASE32)/base32.c \ + $(QRCODEGEN)/qrcodegen.c +BASE64_SRCS := harness_base64.c $(BASE64)/base64.c + +CC := gcc +CSTD := -std=c11 +WARN := -Wall -Wextra + +ASAN_FLAGS := -g -O1 -fsanitize=address,undefined -fno-sanitize-recover=all +VG_FLAGS := -g -O0 +COV_FLAGS := -g -O0 --coverage -fprofile-abs-path + +# lcov 2.x: enable branch coverage everywhere; tolerate the benign +# inconsistencies host coverage of firmware naturally produces. +# +# The valid --ignore-errors category set differs per sub-tool and per lcov +# build: Ubuntu 24.04's lcov 2.0 genhtml rejects 'gcov'/'graph'/'range' +# (there is no gcov phase in genhtml), while lcov/geninfo accept 'gcov' but +# reject 'range'. So keep two lists: the capture/combine list (lcov+geninfo) +# may carry 'gcov'; the genhtml list must not. Every category below exists in +# lcov 2.0.0, so this is robust across lcov 1.x/2.x on Debian and Ubuntu. +LCOV_RC := --rc branch_coverage=1 +LCOV_IGNORE := --ignore-errors unused,empty,mismatch,inconsistent,source,gcov,negative +GENHTML_IGNORE := --ignore-errors unused,empty,mismatch,inconsistent,source,negative +LCOV := lcov $(LCOV_RC) $(LCOV_IGNORE) + +.PHONY: all asan valgrind coverage clean + +all: asan + +# --- ASan + UBSan ----------------------------------------------------------- +asan: $(BUILD)/asan_secret_qr $(BUILD)/asan_base64 + @echo "== running asan/ubsan harnesses ==" + $(BUILD)/asan_secret_qr + $(BUILD)/asan_base64 + @echo "== asan OK ==" + +$(BUILD)/asan_secret_qr: $(SECRET_QR_SRCS) | $(BUILD) + $(CC) $(CSTD) $(WARN) $(ASAN_FLAGS) $(INCLUDES) $(SECRET_QR_SRCS) -o $@ + +$(BUILD)/asan_base64: $(BASE64_SRCS) | $(BUILD) + $(CC) $(CSTD) $(WARN) $(ASAN_FLAGS) $(INCLUDES) $(BASE64_SRCS) -o $@ + +# --- Valgrind --------------------------------------------------------------- +valgrind: $(BUILD)/vg_secret_qr $(BUILD)/vg_base64 + @echo "== running harnesses under valgrind ==" + valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_secret_qr + valgrind --error-exitcode=99 --leak-check=full -q $(BUILD)/vg_base64 + @echo "== valgrind OK ==" + +$(BUILD)/vg_secret_qr: $(SECRET_QR_SRCS) | $(BUILD) + $(CC) $(CSTD) $(WARN) $(VG_FLAGS) $(INCLUDES) $(SECRET_QR_SRCS) -o $@ + +$(BUILD)/vg_base64: $(BASE64_SRCS) | $(BUILD) + $(CC) $(CSTD) $(WARN) $(VG_FLAGS) $(INCLUDES) $(BASE64_SRCS) -o $@ + +# --- Coverage (lcov/genhtml HTML report for the harness-exercised modules) -- +# Build & run the real harnesses with --coverage (producing .gcda), capture +# the run data, filter to first-party sources (dropping the vendored libs and +# the test harnesses themselves), and render coverage/html/ with branches. +# The report covers exactly the modules the harnesses exercise (base32.c, +# base64.c, random.c); HW-only firmware is not built host-side and is absent. +coverage: | $(COV_DIR)/obj + @echo "== coverage: building + running harnesses ==" + $(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(INCLUDES) $(SECRET_QR_SRCS) \ + -o $(COV_DIR)/obj/cov_secret_qr + $(CC) $(CSTD) $(WARN) $(COV_FLAGS) $(INCLUDES) $(BASE64_SRCS) \ + -o $(COV_DIR)/obj/cov_base64 + @echo "== coverage: running instrumented harnesses ==" + $(COV_DIR)/obj/cov_secret_qr + $(COV_DIR)/obj/cov_base64 + @echo "== coverage: capturing run data ==" + $(LCOV) --capture --directory $(COV_DIR)/obj \ + --output-file $(COV_DIR)/run.info + @echo "== coverage: filtering to first-party sources ==" + $(LCOV) --extract $(COV_DIR)/run.info '$(REPO_ROOT)/*' \ + --output-file $(COV_DIR)/first.info + $(LCOV) --remove $(COV_DIR)/first.info \ + '*/libs/qrcodegen/*' '*/libs/littlefs/*' '*/test/*' '/usr/*' \ + --output-file $(COV_DIR)/coverage.info + @echo "== coverage: generating HTML report ==" + genhtml $(LCOV_RC) $(GENHTML_IGNORE) --branch-coverage \ + --legend --title "hslock host coverage" \ + --output-directory $(COV_DIR)/html $(COV_DIR)/coverage.info + @echo "== coverage OK: report at $(COV_DIR)/html/index.html ==" + +$(BUILD): + mkdir -p $(BUILD) + +$(COV_DIR)/obj: + mkdir -p $(COV_DIR)/obj + +clean: + rm -rf $(BUILD) $(COV_DIR) *.gcno *.gcda diff --git a/test/harness_base64.c b/test/harness_base64.c new file mode 100644 index 0000000..a7d6230 --- /dev/null +++ b/test/harness_base64.c @@ -0,0 +1,63 @@ +/* + * Host harness: libs/base64 encode + decode roundtrip. + * Exercises empty input and every input-length residue mod 3 (0/1/2) so that + * both the full-triple loop and the two tail branches of base64_encode, plus + * the padding branches of base64_decode, are covered. Asserts roundtrip + * identity: decode(encode(x)) == x for all cases. + */ + +#include +#include +#include +#include + +#include "base64.h" + +static void roundtrip(const unsigned char *in, size_t in_len) { + char encoded[BASE64_ENCODED_LEN(64)]; + assert(BASE64_ENCODED_LEN(in_len) <= sizeof encoded); + + base64_encode(in, in_len, encoded); + + size_t enc_len = strlen(encoded); + /* Encoded length is always a multiple of 4 (with padding). */ + assert(enc_len % 4 == 0); + assert(enc_len == (in_len + 2) / 3 * 4); + + unsigned char decoded[64]; + int dec_len = base64_decode(encoded, enc_len, decoded); + + assert(dec_len == (int)in_len); + assert(memcmp(in, decoded, in_len) == 0); +} + +int main(void) { + /* Empty input: encoder emits just a null terminator, decoder returns 0. */ + roundtrip((const unsigned char *)"", 0); + + /* Lengths covering every residue mod 3. */ + const char *samples[] = { + "f", /* 1 -> "==" padding branch (single tail byte) */ + "fo", /* 2 -> "=" padding branch (two tail bytes) */ + "foo", /* 3 -> exact triple, no padding */ + "foob", /* 4 */ + "fooba", /* 5 */ + "foobar", /* 6 */ + "hello world", /* 11 */ + }; + for (size_t i = 0; i < sizeof samples / sizeof samples[0]; i++) { + roundtrip((const unsigned char *)samples[i], strlen(samples[i])); + } + + /* Binary payload including a NUL and high bytes. */ + const unsigned char bin[] = {0x00, 0xff, 0x10, 0x80, 0x7f, 0x01, 0xab, 0xcd}; + roundtrip(bin, sizeof bin); + + /* Invalid inputs: bad length and out-of-alphabet char must be rejected. */ + unsigned char scratch[64]; + assert(base64_decode("abc", 3, scratch) == -1); /* length not multiple of 4 */ + assert(base64_decode("ab*d", 4, scratch) == -1); /* '*' not in alphabet */ + + printf("base64 roundtrip OK\n"); + return 0; +} diff --git a/test/harness_secret_qr.c b/test/harness_secret_qr.c new file mode 100644 index 0000000..d22a543 --- /dev/null +++ b/test/harness_secret_qr.c @@ -0,0 +1,53 @@ +/* + * Host harness: secret -> base32 -> otpauth URI -> QR code. + * Mirrors the flow in serial/commands_keys.c (cmd_get_key_secret) using the + * real shared/random, libs/base32 and libs/qrcodegen sources, with a + * deterministic host RNG so the result is reproducible. + */ + +#include +#include +#include +#include + +#include "random.h" /* shared/random.h -> generate_secret */ +#include "base32.h" /* libs/base32 */ +#include "qrcodegen.h" /* libs/qrcodegen/c */ + +#define KEY_SECRET_LEN 20 + +/* Deterministic stand-in for the RP2040 hardware RNG (LCG). */ +static uint64_t g_seed = 0x123456789abcdef0ULL; +uint64_t get_rand_64(void) { + g_seed = g_seed * 6364136223846793005ULL + 1442695040888963407ULL; + return g_seed; +} + +int main(void) { + uint8_t secret[KEY_SECRET_LEN]; + generate_secret(secret); + + char b32[BASE32_ENCODED_LEN(KEY_SECRET_LEN)]; + base32_encode(secret, KEY_SECRET_LEN, b32); + + /* 20 bytes -> ceil(160/5) = 32 base32 chars, no padding. */ + assert(strlen(b32) == 32); + for (size_t i = 0; b32[i]; i++) { + assert(strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", b32[i]) != NULL); + } + + char uri[256]; + snprintf(uri, sizeof uri, "otpauth://totp/hslock:admin?secret=%s&issuer=hslock", b32); + + static uint8_t qr[qrcodegen_BUFFER_LEN_MAX]; + static uint8_t tmp[qrcodegen_BUFFER_LEN_MAX]; + bool ok = qrcodegen_encodeText(uri, tmp, qr, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); + assert(ok); + + int size = qrcodegen_getSize(qr); + assert(size > 0 && size <= 177); + + printf("secret_b32=%s\nqr_size=%d\nOK\n", b32, size); + return 0; +} diff --git a/test/stub/pico/rand.h b/test/stub/pico/rand.h new file mode 100644 index 0000000..7c270c9 --- /dev/null +++ b/test/stub/pico/rand.h @@ -0,0 +1,15 @@ +#ifndef STUB_PICO_RAND_H +#define STUB_PICO_RAND_H + +/* + * Host stub for the Pico SDK's . + * On real hardware get_rand_64() is the RP2040's ROSC-backed RNG; on the + * host each test harness provides its own deterministic definition so that + * secret generation is reproducible. + */ + +#include + +uint64_t get_rand_64(void); /* provided by the host harness */ + +#endif