v0.0.33 - Implement in-place mnemonic entry grid with staged prefix selection and fix stage-3 candidate rendering

This commit is contained in:
Laan Tungir
2026-06-09 10:29:15 -04:00
parent 69c46ab2a7
commit c847179ba4
9 changed files with 859 additions and 224 deletions

View File

@@ -160,6 +160,58 @@ Includes:
- missing policy -> deny path
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity path
## 5.5 USB keyboard firmware workflows in a PCI-USB-controller qube
When a full USB controller is assigned directly to a qube via PCI passthrough, HID input does **not** flow through `sys-usb` input proxy policy. In this setup, keyboard-class USB blocking is usually caused by `usbguard` inside the owning qube.
### Scope check (dom0)
```bash
qvm-prefs AI klass
```
- `StandaloneVM`: service state changes in the qube root filesystem persist.
- `AppVM`: root filesystem state resets at reboot; use `/rw/config/rc.local` for per-qube persistence.
### Disable usbguard immediately (inside AI qube)
```bash
sudo systemctl disable --now usbguard
sudo systemctl mask usbguard
```
### Persist behavior across reboot
- **StandaloneVM**: the mask persists as-is.
- **AppVM**: persist with `/rw/config/rc.local`:
```bash
sudo tee /rw/config/rc.local >/dev/null <<'EOF'
#!/bin/sh
systemctl stop usbguard 2>/dev/null || true
systemctl mask usbguard 2>/dev/null || true
EOF
sudo chmod +x /rw/config/rc.local
```
### Restart + verify
```bash
# dom0
qvm-shutdown --wait AI && qvm-start AI
# inside AI qube
systemctl is-enabled usbguard
lsusb
ls /dev/ttyACM*
```
Expected result: `usbguard` is `masked`, the keyboard-class device enumerates, and serial/WebUSB workflows remain usable.
### Security trade-off
Disabling `usbguard` removes an important BadUSB defense for that qube. Keep this configuration limited to a dedicated hardware-development qube and avoid using it for general browsing or untrusted USB devices.
---
## 6. Security requirements

View File

@@ -0,0 +1,340 @@
# KB2040 Hidden Signer — Display Screens
Reference for the on-device UI so we can iterate on screen designs.
- Panel: **SSD1327 128x128 grayscale OLED** (I2C, STEMMA QT, addr `0x3D`).
- Rendering source: [`display.cpp`](./display.cpp), driven by call sites in [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino).
- Grayscale levels in use:
- dim (headers / outlines): `0x04`
- bar fill: `0x08`
- bright text: `0x0F`
- Each screen historically ended with a bottom **status line** at `y=118`, drawn by `drawStatusLine()`. The status-line writes are currently commented out in the sketch (they were being overwritten by debug/diagnostic prints), so that line now stays effectively empty unless re-enabled.
The boxes below are schematic (the panel is 128x128 px, not character-aligned). They show layout intent, relative position, and the text/labels actually printed.
---
## 1. Volume View
Source: `drawVolumeView()` in [`display.cpp`](./display.cpp:60).
Top-level "media mode" screen. Header switches between `MODE: MEDIA` / `MODE: SIGNER`.
```
+--------------------------------+
| MODE: MEDIA | <- tiny, dim (size 1)
| |
| VOL | <- size 2, dim
| |
| |
| #### #### |
| ## ## # ## | <- "50" centered, size 6
| ## ## # ## | (huge digits)
| ## ## # ## |
| #### #### |
| |
| +------------------------+ | <- volume bar outline
| |########## | | fill = volume %
| +------------------------+ |
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## 2. Signer Menu
Source: `drawSignerMenu()` in [`display.cpp`](./display.cpp:102).
Two items; `>` marks the current selection (shown here on index 0).
```
+--------------------------------+
| SIGNER MENU | <- size 1, dim
| |
| |
| > Generate new | <- size 2, selected
| |
| |
| Enter mnemonic | <- size 2
| |
| |
| |
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## 3. Enter Mnemonic (in-place 2-column grid)
Source (planned): `drawEnterMnemonic()` in [`display.cpp`](./display.cpp).
Replaces the old separate `PICK FIRST LETTER` modal + `WORD PICK` modal with a
single grid that looks like the Generated Phrase screen but is filled in by the
user. Header reads `ENTER MNEMONIC`.
Layout: 2 columns x 6 rows (slots 1-6 left, 7-12 right), same geometry as the
Generated Phrase screen. All 12 numbers are shown; words are blank until entered.
A cursor marks the active slot (starts at slot 1).
```
+--------------------------------+
| ENTER MNEMONIC | <- size 1, dim
|> 1 m 7 | <- cursor '>' on active slot
| 2 8 | slot 1 building: "m"
| 3 9 |
| 4 10 |
| 5 11 |
| 6 12 |
| |
| back letter | <- footer legend (stage-dependent)
+--------------------------------+
```
### Per-word 3-stage entry (in the active slot)
Only the letters chosen so far are shown; a full word appears only at stage 3.
| Stage | Scroll wheel cycles | Active slot shows | PLAY does |
| ----- | ---------------------------------- | ----------------- | -------------------------- |
| 1 | first letter `a`..`z` | `m` | lock letter 1 -> stage 2 |
| 2 | valid 2nd letters for letter 1 | `mo` | lock letter 2 -> stage 3 |
| 3 | words matching first two letters | `mosaic` | select word -> next slot |
- After slot 12's word is selected: build phrase, run
`mnemonic_validate_basic()`, then go to Signer Ready (or INVALID modal).
### BACK behavior (context-sensitive erase)
`BACK` = the NEXT button (PREV stays unused here), consistent with other entry
screens.
| Position | BACK action |
| -------------------------------- | --------------------------------------------- |
| Stage 3 (choosing word) | erase 2nd letter -> return to stage 2 |
| Stage 2 (choosing 2nd letter) | erase 1st letter -> return to stage 1 |
| Stage 1, slot N > 1 | clear previous slot's word, move cursor back to slot N-1 at stage 1 |
| Stage 1, slot 1 | exit to Signer Menu |
### Footer legend (stage-dependent)
| Stage | PREV (left) | PLAY (center) | NEXT (right) |
| ------- | ----------- | ------------- | ------------ |
| Stage 1 | `-` | `letter` | `back` |
| Stage 2 | `-` | `letter` | `back` |
| Stage 3 | `-` | `select` | `back` |
### New wordlist helpers required (in `mnemonic_kb.cpp` / `.h`)
- `mnemonic_second_letters(char first, char *out, uint8_t *count)`
-> set of valid 2nd letters given the 1st letter.
- `mnemonic_prefix_range(char first, char second, uint16_t *start, uint16_t *count)`
-> wordlist index range for all words starting with those two letters.
(`mnemonic_letter_range()` already provides the first-letter range and can back
the stage-1 -> stage-2 transition.)
---
## 4. Mnemonic Page (Generated Phrase)
Source: `drawMnemonicPage()` in [`display.cpp`](./display.cpp:152).
Shows generated phrase in one screen: 2 columns × 6 rows (all 12 words, no spacer rows).
```
+--------------------------------+
| GENERATED PHRASE | <- size 1, dim
| 1 abandon 7 issue | <- size 1, tight rows
| 2 ability 8 jungle |
| 3 able 9 kitchen |
| 4 about 10 lemon |
| 5 above 11 mosquito |
| 6 absent 12 napkin |
| |
| PLAY confirm NEXT cancel | <- size 1
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## 5. Message (generic 2-line modal)
Source: `drawMessage()` in [`display.cpp`](./display.cpp:193).
Generic title + 2 lines. Reused for several prompts.
Example — **Approval prompt** (title `APPROVAL`):
```
+--------------------------------+
| APPROVAL | <- size 1, dim
| |
| |
| PLAY=Allow | <- size 2
| |
| PREV=Deny | <- size 2
| |
| |
| |
| |
| |
| [status line] | <- currently disabled
+--------------------------------+
```
Other current uses of this same screen:
- `PICK FIRST LETTER` with lines `First letter` / `Letter: a`
- `INVALID` with `Bad mnemonic` / `NEXT to return`
- `ERROR` with `Generate failed` / `Try again`
---
## 6. Signer Ready
Source: `drawSignerReady()` in [`display.cpp`](./display.cpp:210).
Shown while waiting for the host to request signing.
```
+--------------------------------+
| SIGNER READY | <- size 1, dim
| |
| +------------------------+ |
| | Awaiting | | <- size 2
| | | |
| | host | | <- size 2
| +------------------------+ |
| |
| NEXT/PLAY: menu | <- size 1
| |
| [status line] | <- currently disabled
+--------------------------------+
```
---
## Notes / observations for redesign
- The bottom status line frequently **duplicated** the in-body hint text (e.g. Word Pick showed `ENC scroll PLAY select` both in the body and in the status line). With status writes disabled, that redundancy is gone.
- `PICK FIRST LETTER` reuses the generic Message screen rather than having a dedicated layout — a candidate for a purpose-built screen.
- All hint text (`ENC` / `PLAY` / `NEXT` / `PREV`) refers to the physical controls: rotary encoder + the PLAY/NEXT/PREV buttons.
```
---
## Screen ↔ render-function map
| Screen | Render function | display.cpp |
| --------------- | --------------------- | ----------- |
| Volume View | `drawVolumeView()` | [60](./display.cpp:60) |
| Signer Menu | `drawSignerMenu()` | [102](./display.cpp:102) |
| Word Pick | `drawWordPick()` | [123](./display.cpp:123) |
| Mnemonic Page | `drawMnemonicPage()` | [152](./display.cpp:152) |
| Message (modal) | `drawMessage()` | [193](./display.cpp:193) |
| Signer Ready | `drawSignerReady()` | [210](./display.cpp:210) |
| Status line | `drawStatusLine()` | [53](./display.cpp:53) |
---
# PROPOSAL: three-zone button legend footer
The three physical buttons sit directly below the screen in this order:
```
PREV PLAY/PAUSE NEXT
^ ^ ^
(left) (center) (right)
```
Idea: replace the free-text status line with a fixed **3-zone legend** along the
bottom of every screen. Each zone shows a single short verb for what that button
does on the current screen — left-aligned, center-aligned, right-aligned so each
label sits over its physical button. No need to print the button name; position
implies it. An empty zone means that button does nothing on that screen.
### Layout math (size 1 font, 6 px/char on a 128 px panel)
- Left zone: left-aligned at `x = 2`.
- Center zone: centered around `x = 64` (use `getTextBounds` to center).
- Right zone: right-aligned to `x = 126` (compute `x = 126 - width`).
- Budget ~7 chars per zone worst case (7 x 6 = 42 px x 3 = 126 px). Keep verbs
<= 7 chars (`select`, `confirm`, `cancel`, `back`, `deny`, `allow`, `next`,
`prev`) to guarantee no overlap.
- Optional: a thin divider line above the legend at `y ~= 112`, and 1 px vertical
ticks between zones to reinforce the 3-button mapping.
```
+--------------------------------+
| ...screen body... |
| |
| -------------------------------| <- optional divider (y~112)
| prev confirm next | <- 3 zones (left/center/right)
+--------------------------------+
^PREV ^PLAY ^NEXT (physical buttons below glass)
```
### Per-screen legend mapping (derived from `signer_mode_tick()`)
Encoder still scrolls/selects on every screen; the legend only describes the 3
buttons. `-` = button does nothing on that screen.
| Screen / state | PREV (left) | PLAY (center) | NEXT (right) |
| -------------------------------------- | ----------- | ------------- | ------------ |
| Volume / Media view | `prev` | `play` | `next` |
| Signer Menu (`SIGNER_UI_MENU`) | `-` | `select` | `-` |
| Generated Phrase (`GENERATE_REVIEW`) | `cancel` | `confirm` | `new` |
| Signer Ready (`READY`) | `-` | `-` | `menu` |
| Letter Pick (`ENTER_LETTER`) | `-` | `confirm` | `back` |
| Word Pick (`ENTER_PICK`) | `-` | `select` | `back` |
| Approval prompt (Message modal) | `deny` | `allow` | `-` |
Notes:
- `PREV` is currently unused in most signer entry screens; it is meaningful in
Media view (`prev` track), the Approval prompt (`deny`), and now the Generated
Phrase screen (`cancel`).
### Behavior change required: Generated Phrase (`SIGNER_UI_GENERATE_REVIEW`)
The legend wording above implies a **functional change** in
[`signer_mode_tick()`](./kb2040_hidden_signer.ino:657), not just labels:
| Button | Current behavior | Proposed behavior |
| ------ | --------------------------- | ------------------------------------------ |
| PREV | (unused) | `cancel` -> return to Signer Menu |
| PLAY | confirm -> save seed | `confirm` -> save seed (unchanged) |
| NEXT | cancel -> Signer Menu | `new` -> regenerate a fresh 12-word phrase and refresh this screen |
Implementation detail for `new`:
- On `s_evt_next_pressed`, call `generate_mnemonic_12(s_generated_mnemonic, ...)`
again, then `signer_enter_review_screen()` to redraw with the new words.
- On `s_evt_prev_pressed`, call `signer_enter_menu_screen()` (the old NEXT path).
> Dependency check (resolved): `s_evt_prev_pressed` already exists and is wired
> the same way as PLAY/NEXT — declared at [line 112](./kb2040_hidden_signer.ino:112),
> reset in [`update_button_events()`](./kb2040_hidden_signer.ino:560), set on PREV
> press at [line 581](./kb2040_hidden_signer.ino:581). No new plumbing needed.
### What this replaces on each screen
- Word Pick body lines `ENC scroll PLAY select` / `NEXT back` -> removed; the
encoder action stays implicit, and PLAY/NEXT move to the footer legend.
- Mnemonic Page body line `PLAY confirm NEXT cancel` -> moves to the footer
legend (frees a body row).
- Signer Ready body line `NEXT/PLAY: menu` -> becomes a single `menu` in the
NEXT zone.
- Approval modal `PLAY=Allow` / `PREV=Deny` body lines -> footer legend
(`deny` left / `allow` center).
### Implementation sketch
1. Add a helper `drawButtonLegend(const char *left, const char *center, const char *right)`
in [`display.cpp`](./display.cpp) that renders the 3 aligned zones (plus the
optional divider/ticks) at the bottom.
2. Give each screen a small static legend (or pass it in) and call
`drawButtonLegend(...)` instead of `drawStatusLine()`.
3. Remove the now-redundant in-body hint rows listed above.
4. Keep `drawStatusLine()` available (commented/disabled) for transient debug.
### Open design decisions (need your call)
1. **NEXT "back" wording**: use the word `back` vs a glyph like `<` / `<<`.
2. **Divider/ticks**: include the thin divider line + inter-zone ticks, or keep
it text-only for a cleaner look.
3. **Empty zones**: render nothing, or a subtle `·` placeholder so the user can
tell the button is intentionally inactive.
4. **Media view**: apply the same legend (`prev` / `play` / `next`), or leave the
Volume view visually as-is since its meaning is already obvious.

View File

@@ -35,7 +35,7 @@ This directory contains an Arduino firmware scaffold that implements the `kb2040
3. Wire exact `keyboard_modules` pin mapping and display abstraction.
4. Add secure memory wiping and persistent policy storage if required.
## Arduino setup
## Arduino setup (IDE)
1. Install Adafruit board index in Arduino IDE.
2. Install **Adafruit RP2040** boards package.
@@ -44,6 +44,25 @@ This directory contains an Arduino firmware scaffold that implements the `kb2040
5. Install library **Adafruit TinyUSB Library**.
6. Open and flash [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino).
## Arduino CLI flashing (recommended repeatable command)
Use this exact command when flashing from CLI:
```bash
arduino-cli compile --upload -p /dev/ttyACM0 \
--fqbn rp2040:rp2040:adafruit_kb2040:usbstack=tinyusb \
firmware/kb2040_hidden_signer
```
Notes:
- `usbstack=tinyusb` is **required** for this firmware (TinyUSB composite device).
- If you omit that option, build fails with `TinyUSB is not selected`.
- If `/dev/ttyACM0` changes, find the current port with:
```bash
arduino-cli board list
```
## Host validation
Install dependency:

View File

@@ -26,19 +26,20 @@ bool g_signerMode = false;
char g_status[24] = "BOOT";
uint8_t g_menuSelected = 0;
uint8_t g_wordSlot = 0;
uint8_t g_wordTotal = 12;
uint16_t g_wordIndex = 0;
char g_word[16] = "abandon";
uint8_t g_mnemonicPage = 0;
char g_mnemonic[256] = "";
char g_msgTitle[18] = "";
char g_msgLine1[22] = "";
char g_msgLine2[22] = "";
char g_enterSlots[12][16] = {{0}};
uint8_t g_enterActiveSlot = 0;
uint8_t g_enterStage = 1;
char g_enterBuilding[16] = "";
uint8_t g_enterCursorPos = 0;
enum signer_screen_t {
SIGNER_SCREEN_MENU = 0,
SIGNER_SCREEN_WORD_PICK = 1,
SIGNER_SCREEN_ENTER_GRID = 1,
SIGNER_SCREEN_MNEMONIC_PAGE = 2,
SIGNER_SCREEN_MESSAGE = 3,
SIGNER_SCREEN_READY = 4,
@@ -57,12 +58,38 @@ void drawStatusLine() {
g_display.print(g_status);
}
void drawButtonLegend(const char *left, const char *center, const char *right) {
g_display.setTextSize(1);
g_display.setTextColor(kGrayText);
g_display.setTextWrap(false);
if (left && left[0] != '\0') {
g_display.setCursor(2, 118);
g_display.print(left);
}
int16_t x1 = 0;
int16_t y1 = 0;
uint16_t w = 0;
uint16_t h = 0;
if (center && center[0] != '\0') {
g_display.getTextBounds(center, 0, 0, &x1, &y1, &w, &h);
const int16_t x = static_cast<int16_t>((kDisplayWidth - w) / 2);
g_display.setCursor(x, 118);
g_display.print(center);
}
if (right && right[0] != '\0') {
g_display.getTextBounds(right, 0, 0, &x1, &y1, &w, &h);
const int16_t x = static_cast<int16_t>(126 - w);
g_display.setCursor(x, 118);
g_display.print(right);
}
}
void drawVolumeView() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(6, 4);
g_display.print(g_signerMode ? "MODE: SIGNER" : "MODE: MEDIA");
g_display.setTextColor(kGrayDim);
g_display.setTextSize(2);
@@ -96,7 +123,7 @@ void drawVolumeView() {
g_display.fillRect(barX + 1, barY + 1, fillW, barH - 2, kGrayBar);
}
drawStatusLine();
drawButtonLegend("prev", "play", "next");
}
void drawSignerMenu() {
@@ -117,36 +144,76 @@ void drawSignerMenu() {
g_display.print(items[i]);
}
drawStatusLine();
drawButtonLegend("", "select", "");
}
void drawWordPick() {
void drawEnterMnemonic() {
g_display.setTextWrap(false);
g_display.setTextSize(1);
g_display.setTextColor(kGrayDim);
g_display.setCursor(2, 2);
char hdr[20];
snprintf(hdr, sizeof(hdr), "WORD %u/%u", (unsigned)(g_wordSlot + 1), (unsigned)g_wordTotal);
g_display.print(hdr);
g_display.print("ENTER MNEMONIC");
char idxBuf[20];
snprintf(idxBuf, sizeof(idxBuf), "IDX %u", (unsigned)g_wordIndex);
g_display.setCursor(82, 2);
g_display.print(idxBuf);
g_display.drawRect(4, 24, 120, 56, kGrayDim);
g_display.setTextSize(2);
g_display.setTextColor(kGrayText);
g_display.setCursor(10, 44);
g_display.print(g_word);
for (uint8_t i = 0; i < 12; ++i) {
const uint8_t row = (i < 6) ? i : (i - 6);
const int16_t x = (i < 6) ? 0 : 64;
const int16_t y = static_cast<int16_t>(18 + row * 9);
g_display.setTextSize(1);
g_display.setCursor(6, 88);
g_display.print("ENC scroll PLAY select");
g_display.setCursor(6, 98);
g_display.print("NEXT back");
const bool isActive = (i == g_enterActiveSlot);
const char cursor = isActive ? '>' : ' ';
drawStatusLine();
const char *slotWord = g_enterSlots[i];
if (!slotWord) {
slotWord = "";
}
char activeBuf[16];
const char *shownWord = slotWord;
if (isActive) {
const char *src = g_enterBuilding;
if (!src) {
src = "";
}
if (g_enterStage < 3) {
size_t srcLen = strlen(src);
if (srcLen > 14) {
srcLen = 14;
}
size_t pos = g_enterCursorPos;
if (pos > srcLen) {
pos = srcLen;
}
size_t out = 0;
for (size_t j = 0; j < pos && out < sizeof(activeBuf) - 1; ++j) {
activeBuf[out++] = src[j];
}
if (out < sizeof(activeBuf) - 1) {
activeBuf[out++] = '_';
}
for (size_t j = pos; j < srcLen && out < sizeof(activeBuf) - 1; ++j) {
activeBuf[out++] = src[j];
}
activeBuf[out] = '\0';
shownWord = activeBuf;
} else if (src[0] != '\0') {
shownWord = src;
}
}
char lineBuf[22];
snprintf(lineBuf, sizeof(lineBuf), "%c%2u %s", cursor, (unsigned)(i + 1), shownWord);
g_display.setCursor(x, y);
g_display.print(lineBuf);
}
if (g_enterStage == 3) {
drawButtonLegend("", "select", "back");
} else {
drawButtonLegend("", "letter", "back");
}
}
void drawMnemonicPage() {
@@ -160,34 +227,28 @@ void drawMnemonicPage() {
strncpy(copy, g_mnemonic, sizeof(copy) - 1);
copy[sizeof(copy) - 1] = '\0';
const uint8_t perPage = 4;
const uint8_t startWord = g_mnemonicPage * perPage;
const uint8_t endWord = startWord + perPage;
uint8_t wordNum = 0;
char *words[12] = {nullptr};
uint8_t count = 0;
char *saveptr = nullptr;
char *tok = strtok_r(copy, " ", &saveptr);
uint8_t line = 0;
while (tok) {
if (wordNum >= startWord && wordNum < endWord) {
char lineBuf[32];
snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(wordNum + 1), tok);
g_display.setTextColor(kGrayText);
g_display.setCursor(8, 24 + line * 18);
g_display.print(lineBuf);
line++;
}
wordNum++;
while (tok && count < 12) {
words[count++] = tok;
tok = strtok_r(nullptr, " ", &saveptr);
}
g_display.setTextSize(1);
g_display.setCursor(6, 98);
g_display.print("ENC page PLAY confirm");
g_display.setCursor(6, 108);
g_display.print("NEXT cancel");
g_display.setTextColor(kGrayText);
for (uint8_t i = 0; i < count; ++i) {
const uint8_t row = (i < 6) ? i : (i - 6);
const int16_t x = (i < 6) ? 0 : 64;
const int16_t y = static_cast<int16_t>(18 + row * 9);
drawStatusLine();
char lineBuf[20];
snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(i + 1), words[i]);
g_display.setCursor(x, y);
g_display.print(lineBuf);
}
drawButtonLegend("cancel", "confirm", "new");
}
void drawMessage() {
@@ -204,7 +265,21 @@ void drawMessage() {
g_display.setCursor(6, 62);
g_display.print(g_msgLine2);
drawStatusLine();
const char *left = "";
const char *center = "";
const char *right = "";
if (strcmp(g_msgTitle, "APPROVAL") == 0) {
left = "deny";
center = "allow";
} else if (strcmp(g_msgTitle, "INVALID") == 0) {
center = "select";
right = "back";
} else if (strcmp(g_msgTitle, "ERROR") == 0) {
center = "select";
}
drawButtonLegend(left, center, right);
}
void drawSignerReady() {
@@ -223,11 +298,7 @@ void drawSignerReady() {
g_display.setCursor(30, 58);
g_display.print("host");
g_display.setTextSize(1);
g_display.setCursor(6, 96);
g_display.print("NEXT/PLAY: menu");
drawStatusLine();
drawButtonLegend("", "", "menu");
}
void renderFrame() {
@@ -244,8 +315,8 @@ void renderFrame() {
case SIGNER_SCREEN_MENU:
drawSignerMenu();
break;
case SIGNER_SCREEN_WORD_PICK:
drawWordPick();
case SIGNER_SCREEN_ENTER_GRID:
drawEnterMnemonic();
break;
case SIGNER_SCREEN_MNEMONIC_PAGE:
drawMnemonicPage();
@@ -335,12 +406,22 @@ void display_show_signer_menu(uint8_t selected_index) {
g_dirty = true;
}
void display_show_word_pick(uint8_t slot_index, uint8_t total_slots, const char *word, uint16_t word_index) {
g_signerScreen = SIGNER_SCREEN_WORD_PICK;
g_wordSlot = slot_index;
g_wordTotal = total_slots;
g_wordIndex = word_index;
copy_text(g_word, sizeof(g_word), word);
void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage,
const char *building_text, uint8_t cursor_pos) {
g_signerScreen = SIGNER_SCREEN_ENTER_GRID;
g_enterActiveSlot = (active_slot > 11) ? 11 : active_slot;
g_enterStage = stage;
g_enterCursorPos = cursor_pos;
copy_text(g_enterBuilding, sizeof(g_enterBuilding), building_text);
for (uint8_t i = 0; i < 12; ++i) {
const char *src = "";
if (slots && slots[i]) {
src = slots[i];
}
copy_text(g_enterSlots[i], sizeof(g_enterSlots[i]), src);
}
g_dirty = true;
}
@@ -364,6 +445,9 @@ void display_show_signer_ready() {
g_dirty = true;
}
// Note: status-line updates are currently disabled at call sites in
// kb2040_hidden_signer.ino by commenting out display_set_status(...)
// invocations to keep the bottom status line from being overwritten.
void display_set_status(const char *status) {
if (!status) {
return;

View File

@@ -25,7 +25,8 @@ void display_set_volume(uint8_t volume);
// Signer UI rendering helpers.
void display_show_signer_menu(uint8_t selected_index);
void display_show_word_pick(uint8_t slot_index, uint8_t total_slots, const char *word, uint16_t word_index);
void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage,
const char *building_text, uint8_t cursor_pos);
void display_show_mnemonic_page(const char *mnemonic, uint8_t page_index);
void display_show_message(const char *title, const char *line1, const char *line2);
void display_show_signer_ready();

View File

@@ -52,8 +52,7 @@ enum signer_ui_state_t {
SIGNER_UI_MENU = 0,
SIGNER_UI_READY = 1,
SIGNER_UI_GENERATE_REVIEW = 2,
SIGNER_UI_ENTER_LETTER = 3,
SIGNER_UI_ENTER_PICK = 4,
SIGNER_UI_ENTER_GRID = 3,
};
static device_mode_t s_mode = MODE_MEDIA;
@@ -67,12 +66,19 @@ static bool s_auto_approve = false;
// Signer UI state
static uint8_t s_signer_menu_selected = 0; // 0=Generate, 1=Enter
static uint8_t s_signer_review_page = 0; // 0..2 for 12 words at 4/page
static uint8_t s_signer_review_page = 0; // legacy placeholder (review now renders all 12 words at once)
static char s_generated_mnemonic[256] = "";
static uint8_t s_enter_slot = 0; // 0..11
static uint8_t s_enter_letter_index = 0; // 0..25 => a..z
static uint16_t s_enter_word_index = 0; // current picker index
static uint8_t s_enter_stage = 1; // 1=first letter, 2=second letter, 3=word select
static uint8_t s_enter_first_letter_index = 0; // 0..25 => a..z
static char s_enter_second_letters[26] = {0};
static uint8_t s_enter_second_count = 0;
static uint8_t s_enter_second_index = 0;
static uint16_t s_enter_prefix_start = 0;
static uint16_t s_enter_prefix_count = 0;
static uint16_t s_enter_candidate_offset = 0;
static uint16_t s_enter_selected_indices[12] = {0};
static bool s_enter_committed[12] = {false};
static void signer_enter_ready_screen();
@@ -128,8 +134,9 @@ static void led_blink(int n, int on_ms, int off_ms) {
static void send_consumer_key(uint16_t usage) {
if (!usb_hid.ready()) return;
usb_hid.sendReport16(1, usage);
delay(2);
delay(8); // restore hold for better throughput while keeping reliability
usb_hid.sendReport16(1, 0);
delay(4); // restore smaller inter-key gap for faster throughput
}
static int8_t read_ab() {
@@ -252,30 +259,30 @@ static void signer_apply_seed(const char *mnemonic) {
strncpy(s_mnemonic, mnemonic, sizeof(s_mnemonic) - 1);
s_mnemonic[sizeof(s_mnemonic) - 1] = '\0';
s_seed_loaded = strlen(s_mnemonic) > 0;
display_set_status(s_seed_loaded ? "Seed loaded" : "Seed cleared");
// display_set_status(s_seed_loaded ? "Seed loaded" : "Seed cleared");
}
static bool wait_for_user_approval(uint32_t timeout_ms) {
display_show_message("APPROVAL", "PLAY=Allow", "PREV=Deny");
display_set_status("Waiting approval");
display_show_message("APPROVAL", "Host requests", "signature");
// display_set_status("Waiting approval");
uint32_t start = millis();
while ((millis() - start) < timeout_ms) {
if (btn_pressed(PIN_BTN_PLAY)) {
while (btn_pressed(PIN_BTN_PLAY)) delay(5);
display_set_status("Approved");
// display_set_status("Approved");
return true;
}
if (btn_pressed(PIN_BTN_PREV)) {
while (btn_pressed(PIN_BTN_PREV)) delay(5);
display_set_status("Denied");
// display_set_status("Denied");
return false;
}
display_tick();
delay(5);
}
display_set_status("Approval timeout");
// display_set_status("Approval timeout");
return false;
}
@@ -293,13 +300,13 @@ static void handle_rpc(const char *req_json) {
char method[64] = {0};
if (!json_extract_string(req_json, "method", method, sizeof(method))) {
send_json_response("{\"error\":{\"code\":-32600,\"message\":\"invalid request\"}}");
display_set_status("RPC invalid request");
// display_set_status("RPC invalid request");
return;
}
if (strcmp(method, "ping") == 0) {
send_json_response("{\"result\":\"pong\"}");
display_set_status("RPC ping");
// display_set_status("RPC ping");
return;
}
@@ -311,7 +318,7 @@ static void handle_rpc(const char *req_json) {
s_seed_loaded ? "true" : "false",
s_auto_approve ? "true" : "false");
send_json_response(resp);
display_set_status("RPC get_status");
// display_set_status("RPC get_status");
return;
}
@@ -319,7 +326,7 @@ static void handle_rpc(const char *req_json) {
char phrase[256] = {0};
if (!json_extract_string(req_json, "mnemonic", phrase, sizeof(phrase))) {
send_json_response("{\"error\":{\"code\":-32602,\"message\":\"missing mnemonic\"}}");
display_set_status("Mnemonic missing");
// display_set_status("Mnemonic missing");
return;
}
@@ -335,14 +342,14 @@ static void handle_rpc(const char *req_json) {
if (strstr(req_json, "\"value\":true") != nullptr) {
s_auto_approve = true;
send_json_response("{\"result\":true}");
display_set_status("Auto-approve ON");
// display_set_status("Auto-approve ON");
} else if (strstr(req_json, "\"value\":false") != nullptr) {
s_auto_approve = false;
send_json_response("{\"result\":false}");
display_set_status("Auto-approve OFF");
// display_set_status("Auto-approve OFF");
} else {
send_json_response("{\"error\":{\"code\":-32602,\"message\":\"missing value\"}}");
display_set_status("Auto-approve invalid");
// display_set_status("Auto-approve invalid");
}
return;
}
@@ -350,7 +357,7 @@ static void handle_rpc(const char *req_json) {
if (strcmp(method, "get_public_key") == 0) {
if (!s_seed_loaded) {
send_json_response("{\"error\":{\"code\":2014,\"message\":\"seed not loaded\"}}");
display_set_status("No seed");
// display_set_status("No seed");
return;
}
@@ -360,20 +367,20 @@ static void handle_rpc(const char *req_json) {
char resp[192];
snprintf(resp, sizeof(resp), "{\"result\":{\"pubkey\":\"%s\"}}", pubhex);
send_json_response(resp);
display_set_status("Pubkey served");
// display_set_status("Pubkey served");
return;
}
if (strcmp(method, "sign_event") == 0) {
if (!s_seed_loaded) {
send_json_response("{\"error\":{\"code\":2014,\"message\":\"seed not loaded\"}}");
display_set_status("Sign blocked:no seed");
// display_set_status("Sign blocked:no seed");
return;
}
if (s_mode != MODE_SIGNER) {
send_json_response("{\"error\":{\"code\":2015,\"message\":\"not in signer mode\"}}");
display_set_status("Sign blocked:mode");
// display_set_status("Sign blocked:mode");
return;
}
@@ -390,7 +397,7 @@ static void handle_rpc(const char *req_json) {
}
send_json_response("{\"result\":{\"sig\":\"dev_signature_placeholder\"}}");
display_set_status("Event signed");
// display_set_status("Event signed");
if (s_signer_ui_state == SIGNER_UI_MENU) {
display_show_signer_menu(s_signer_menu_selected);
} else if (s_signer_ui_state == SIGNER_UI_READY) {
@@ -400,7 +407,7 @@ static void handle_rpc(const char *req_json) {
}
send_json_response("{\"error\":{\"code\":-32601,\"message\":\"method not found\"}}");
display_set_status("RPC method missing");
// display_set_status("RPC method missing");
}
static void webusb_pump_frames() {
@@ -426,7 +433,7 @@ static void webusb_pump_frames() {
if (body_len > (sizeof(s_rx_buf) - 4)) {
s_rx_len = 0;
send_json_response("{\"error\":{\"code\":-32000,\"message\":\"frame too large\"}}");
display_set_status("Frame too large");
// display_set_status("Frame too large");
return;
}
@@ -452,56 +459,96 @@ static void signer_enter_menu_screen() {
s_signer_ui_state = SIGNER_UI_MENU;
display_set_view(DISPLAY_VIEW_SIGNER);
display_show_signer_menu(s_signer_menu_selected);
display_set_status("ENC scroll PLAY select");
// display_set_status("ENC scroll PLAY select");
}
static void signer_enter_ready_screen() {
s_signer_ui_state = SIGNER_UI_READY;
display_set_view(DISPLAY_VIEW_SIGNER);
display_show_signer_ready();
display_set_status("Ready to sign");
// display_set_status("Ready to sign");
}
static char signer_current_letter() {
return (char)('a' + (s_enter_letter_index % 26));
static char signer_first_letter() {
return (char)('a' + (s_enter_first_letter_index % 26));
}
static void signer_enter_letter_pick_screen() {
s_signer_ui_state = SIGNER_UI_ENTER_LETTER;
char line1[20];
char line2[24];
snprintf(line1, sizeof(line1), "Word %u/12", (unsigned)(s_enter_slot + 1));
snprintf(line2, sizeof(line2), "Letter: %c", signer_current_letter());
display_show_message("PICK FIRST LETTER", line1, line2);
display_set_status("ENC letter PLAY confirm");
static char signer_second_letter() {
if (s_enter_second_count == 0 || s_enter_second_index >= s_enter_second_count) {
return 'a';
}
return s_enter_second_letters[s_enter_second_index];
}
static void signer_enter_word_pick_screen() {
s_signer_ui_state = SIGNER_UI_ENTER_PICK;
uint16_t start = 0;
uint16_t count = 0;
if (!mnemonic_letter_range(signer_current_letter(), &start, &count) || count == 0) {
start = 0;
count = 2048;
static void signer_reset_active_slot_state() {
s_enter_stage = 1;
s_enter_first_letter_index = 0;
s_enter_second_count = 0;
s_enter_second_index = 0;
s_enter_prefix_start = 0;
s_enter_prefix_count = 0;
s_enter_candidate_offset = 0;
}
if (s_enter_word_index < start || s_enter_word_index >= (uint16_t)(start + count)) {
s_enter_word_index = start;
static bool signer_prepare_second_letters() {
s_enter_second_count = 0;
s_enter_second_index = 0;
return mnemonic_second_letters(signer_first_letter(), s_enter_second_letters, &s_enter_second_count) && s_enter_second_count > 0;
}
const char *word = mnemonic_word_at(s_enter_word_index);
if (!word) word = "abandon";
display_show_word_pick(s_enter_slot, 12, word, s_enter_word_index);
display_set_status("ENC word PLAY select");
static bool signer_prepare_prefix_candidates() {
s_enter_prefix_start = 0;
s_enter_prefix_count = 0;
s_enter_candidate_offset = 0;
return mnemonic_prefix_range(signer_first_letter(), signer_second_letter(), &s_enter_prefix_start, &s_enter_prefix_count) &&
s_enter_prefix_count > 0;
}
static void signer_render_enter_grid() {
const char *slot_words[12] = {nullptr};
for (uint8_t i = 0; i < 12; ++i) {
if (s_enter_committed[i]) {
slot_words[i] = mnemonic_word_at(s_enter_selected_indices[i]);
} else {
slot_words[i] = "";
}
}
char building[16] = "";
uint8_t cursor_pos = 0;
if (s_enter_stage == 1) {
building[0] = signer_first_letter();
building[1] = '\0';
cursor_pos = 1;
} else if (s_enter_stage == 2) {
building[0] = signer_first_letter();
building[1] = signer_second_letter();
building[2] = '\0';
cursor_pos = 2;
} else {
const uint16_t idx = (uint16_t)(s_enter_prefix_start + s_enter_candidate_offset);
const char *word = mnemonic_word_at(idx);
if (word) {
strncpy(building, word, sizeof(building) - 1);
building[sizeof(building) - 1] = '\0';
}
cursor_pos = (uint8_t)strlen(building);
}
display_show_enter_mnemonic(slot_words, s_enter_slot, s_enter_stage, building, cursor_pos);
}
static void signer_enter_grid_screen() {
s_signer_ui_state = SIGNER_UI_ENTER_GRID;
display_set_view(DISPLAY_VIEW_SIGNER);
signer_render_enter_grid();
}
static void signer_enter_review_screen() {
s_signer_ui_state = SIGNER_UI_GENERATE_REVIEW;
if (s_signer_review_page > 2) s_signer_review_page = 2;
display_show_mnemonic_page(s_generated_mnemonic, s_signer_review_page);
display_set_status("PLAY confirm NEXT cancel");
display_show_mnemonic_page(s_generated_mnemonic, 0);
// display_set_status("PLAY confirm NEXT cancel");
}
static void build_entered_mnemonic(char *out, size_t out_sz) {
@@ -513,6 +560,10 @@ static void build_entered_mnemonic(char *out, size_t out_sz) {
size_t cursor = 0;
for (uint8_t i = 0; i < 12; ++i) {
if (!s_enter_committed[i]) {
return;
}
const char *w = mnemonic_word_at(s_enter_selected_indices[i]);
if (!w) w = "abandon";
@@ -544,10 +595,10 @@ static void handle_mode_switch_chord() {
if (s_mode == MODE_SIGNER) {
display_set_view(DISPLAY_VIEW_SIGNER);
signer_enter_menu_screen();
display_set_status("Signer mode");
// display_set_status("Signer mode");
} else {
display_set_view(DISPLAY_VIEW_VOLUME);
display_set_status("Media mode");
// display_set_status("Media mode");
}
led_blink(s_mode == MODE_SIGNER ? 3 : 1, 80, 80);
@@ -588,36 +639,32 @@ static void media_mode_tick() {
const int32_t steps = encoder_take_steps();
if (steps > 0) {
for (int32_t i = 0; i < steps; ++i) {
const int8_t applied = volume_change_by(+1);
if (applied > 0) {
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_INCREMENT);
volume_change_by(+1); // keep display clamped to 0..100
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_INCREMENT); // always emit HID step
display_set_volume(s_local_volume);
}
}
display_set_status("Volume up");
// display_set_status("Volume up");
} else if (steps < 0) {
for (int32_t i = 0; i < -steps; ++i) {
const int8_t applied = volume_change_by(-1);
if (applied < 0) {
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_DECREMENT);
volume_change_by(-1); // keep display clamped to 0..100
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_DECREMENT); // always emit HID step
display_set_volume(s_local_volume);
}
}
display_set_status("Volume down");
// display_set_status("Volume down");
}
if (s_evt_play_pressed) {
send_consumer_key(HID_USAGE_CONSUMER_PLAY_PAUSE);
display_set_status("Play/Pause");
// display_set_status("Play/Pause");
}
if (s_evt_next_pressed) {
// Preserve original hardware remap: NEXT wiring is swapped.
send_consumer_key(HID_USAGE_CONSUMER_SCAN_PREVIOUS_TRACK);
display_set_status("Prev track");
// display_set_status("Prev track");
}
if (s_evt_prev_pressed) {
send_consumer_key(HID_USAGE_CONSUMER_SCAN_NEXT_TRACK);
display_set_status("Next track");
// display_set_status("Next track");
}
}
@@ -646,27 +693,22 @@ static void signer_mode_tick() {
signer_enter_review_screen();
} else {
display_show_message("ERROR", "Generate failed", "Try again");
display_set_status("Generate failed");
// display_set_status("Generate failed");
}
} else {
s_enter_slot = 0;
s_enter_letter_index = 0;
s_enter_word_index = s_enter_selected_indices[s_enter_slot];
signer_enter_letter_pick_screen();
for (uint8_t i = 0; i < 12; ++i) {
s_enter_committed[i] = false;
s_enter_selected_indices[i] = 0;
}
signer_reset_active_slot_state();
signer_enter_grid_screen();
}
}
break;
}
case SIGNER_UI_GENERATE_REVIEW: {
if (steps != 0) {
int16_t p = (int16_t)s_signer_review_page + (steps > 0 ? 1 : -1);
if (p < 0) p = 0;
if (p > 2) p = 2;
s_signer_review_page = (uint8_t)p;
signer_enter_review_screen();
}
if (s_evt_play_pressed) {
signer_apply_seed(s_generated_mnemonic);
signer_enter_ready_screen();
@@ -674,7 +716,15 @@ static void signer_mode_tick() {
if (s_evt_next_pressed) {
signer_enter_menu_screen();
display_set_status("Generate canceled");
// display_set_status("Generate canceled");
}
if (s_evt_prev_pressed) {
if (generate_mnemonic_12(s_generated_mnemonic, sizeof(s_generated_mnemonic)) == 0) {
signer_enter_review_screen();
} else {
display_show_message("ERROR", "Generate failed", "Try again");
}
}
break;
}
@@ -686,69 +736,49 @@ static void signer_mode_tick() {
break;
}
case SIGNER_UI_ENTER_LETTER: {
case SIGNER_UI_ENTER_GRID: {
if (steps != 0) {
int16_t idx = (int16_t)s_enter_letter_index + (steps > 0 ? 1 : -1);
if (s_enter_stage == 1) {
int16_t idx = (int16_t)s_enter_first_letter_index + (steps > 0 ? 1 : -1);
while (idx < 0) idx += 26;
while (idx >= 26) idx -= 26;
s_enter_letter_index = (uint8_t)idx;
signer_enter_letter_pick_screen();
s_enter_first_letter_index = (uint8_t)idx;
} else if (s_enter_stage == 2) {
if (s_enter_second_count > 0) {
int16_t idx = (int16_t)s_enter_second_index + (steps > 0 ? 1 : -1);
while (idx < 0) idx += s_enter_second_count;
while (idx >= s_enter_second_count) idx -= s_enter_second_count;
s_enter_second_index = (uint8_t)idx;
}
} else if (s_enter_prefix_count > 0) {
int32_t rel = (int32_t)s_enter_candidate_offset + steps;
while (rel < 0) rel += s_enter_prefix_count;
while (rel >= s_enter_prefix_count) rel -= s_enter_prefix_count;
s_enter_candidate_offset = (uint16_t)rel;
}
signer_render_enter_grid();
}
if (s_evt_play_pressed) {
uint16_t start = 0;
uint16_t count = 0;
if (mnemonic_letter_range(signer_current_letter(), &start, &count) && count > 0) {
s_enter_word_index = start;
if (s_enter_stage == 1) {
if (signer_prepare_second_letters()) {
s_enter_stage = 2;
signer_render_enter_grid();
}
} else if (s_enter_stage == 2) {
if (signer_prepare_prefix_candidates()) {
s_enter_stage = 3;
signer_render_enter_grid();
}
} else {
s_enter_word_index = 0;
}
signer_enter_word_pick_screen();
}
if (s_evt_next_pressed) {
if (s_enter_slot == 0) {
signer_enter_menu_screen();
display_set_status("Entry canceled");
} else {
s_enter_slot--;
const char *prev_word = mnemonic_word_at(s_enter_selected_indices[s_enter_slot]);
if (prev_word && prev_word[0] >= 'a' && prev_word[0] <= 'z') {
s_enter_letter_index = (uint8_t)(prev_word[0] - 'a');
}
s_enter_word_index = s_enter_selected_indices[s_enter_slot];
signer_enter_word_pick_screen();
display_set_status("Back one step");
}
}
break;
}
case SIGNER_UI_ENTER_PICK: {
uint16_t start = 0;
uint16_t count = 0;
if (!mnemonic_letter_range(signer_current_letter(), &start, &count) || count == 0) {
start = 0;
count = 2048;
}
if (steps != 0) {
int32_t rel = (int32_t)s_enter_word_index - (int32_t)start;
rel += steps;
while (rel < 0) rel += count;
while (rel >= count) rel -= count;
s_enter_word_index = (uint16_t)(start + rel);
signer_enter_word_pick_screen();
}
if (s_evt_play_pressed) {
s_enter_selected_indices[s_enter_slot] = s_enter_word_index;
s_enter_selected_indices[s_enter_slot] = (uint16_t)(s_enter_prefix_start + s_enter_candidate_offset);
s_enter_committed[s_enter_slot] = true;
if (s_enter_slot < 11) {
s_enter_slot++;
s_enter_letter_index = 0;
s_enter_word_index = s_enter_selected_indices[s_enter_slot];
signer_enter_letter_pick_screen();
signer_reset_active_slot_state();
signer_render_enter_grid();
} else {
char phrase[256];
build_entered_mnemonic(phrase, sizeof(phrase));
@@ -756,15 +786,30 @@ static void signer_mode_tick() {
signer_apply_seed(phrase);
signer_enter_ready_screen();
} else {
display_show_message("INVALID", "Bad mnemonic", "NEXT to return");
display_set_status("Validation failed");
display_show_message("INVALID", "Bad mnemonic", "Fix and retry");
}
}
}
}
if (s_evt_next_pressed) {
signer_enter_letter_pick_screen();
display_set_status("Back to letter");
if (s_enter_stage == 3) {
s_enter_stage = 2;
signer_render_enter_grid();
} else if (s_enter_stage == 2) {
s_enter_stage = 1;
signer_render_enter_grid();
} else {
if (s_enter_slot == 0) {
signer_enter_menu_screen();
} else {
s_enter_slot--;
s_enter_committed[s_enter_slot] = false;
s_enter_selected_indices[s_enter_slot] = 0;
signer_reset_active_slot_state();
signer_render_enter_grid();
}
}
}
break;
}
@@ -808,7 +853,7 @@ void setup() {
display_set_mode(false);
display_set_view(DISPLAY_VIEW_VOLUME);
display_set_status("USB mounted");
// display_set_status("USB mounted");
digitalWrite(LED_BUILTIN, HIGH);
led_blink(2, 60, 60);
@@ -821,7 +866,7 @@ void loop() {
display_set_mode(s_mode == MODE_SIGNER);
display_set_volume(s_local_volume);
display_set_view(s_mode == MODE_SIGNER ? DISPLAY_VIEW_SIGNER : DISPLAY_VIEW_VOLUME);
display_set_status("Ready");
// display_set_status("Ready");
if (s_mode == MODE_SIGNER) {
signer_enter_menu_screen();
}
@@ -860,7 +905,7 @@ void loop() {
(unsigned long)s_diag_bytes_rx,
(unsigned long)s_diag_frames_rx,
(unsigned long)s_diag_responses_tx);
display_set_status(diag);
// display_set_status(diag);
// Idle LED state: ON (HIGH) when connected, OFF otherwise. The RX path
// drives it LOW briefly on each byte, so a flicker == bytes arriving.

View File

@@ -84,6 +84,98 @@ bool mnemonic_letter_range(char letter, uint16_t *out_start, uint16_t *out_count
return true;
}
bool mnemonic_second_letters(char first, char *out, uint8_t *count) {
if (!out || !count) {
return false;
}
*count = 0;
if (first >= 'A' && first <= 'Z') {
first = (char)(first - 'A' + 'a');
}
if (first < 'a' || first > 'z') {
return false;
}
uint16_t start = 0;
uint16_t range_count = 0;
if (!mnemonic_letter_range(first, &start, &range_count) || range_count == 0) {
return false;
}
bool seen[26] = {false};
uint8_t found = 0;
for (uint16_t i = 0; i < range_count; ++i) {
const char* w = BIP39_WORDLIST[start + i];
char c = w[1];
if (c >= 'A' && c <= 'Z') {
c = (char)(c - 'A' + 'a');
}
if (c < 'a' || c > 'z') {
continue;
}
const uint8_t idx = (uint8_t)(c - 'a');
if (!seen[idx]) {
seen[idx] = true;
out[found++] = c;
}
}
*count = found;
return found > 0;
}
bool mnemonic_prefix_range(char first, char second, uint16_t *out_start, uint16_t *out_count) {
if (!out_start || !out_count) {
return false;
}
if (first >= 'A' && first <= 'Z') {
first = (char)(first - 'A' + 'a');
}
if (second >= 'A' && second <= 'Z') {
second = (char)(second - 'A' + 'a');
}
if (first < 'a' || first > 'z' || second < 'a' || second > 'z') {
return false;
}
uint16_t start = 0;
uint16_t range_count = 0;
if (!mnemonic_letter_range(first, &start, &range_count) || range_count == 0) {
return false;
}
int first_match = -1;
int last_match = -1;
for (uint16_t i = 0; i < range_count; ++i) {
const char* w = BIP39_WORDLIST[start + i];
char c = w[1];
if (c >= 'A' && c <= 'Z') {
c = (char)(c - 'A' + 'a');
}
if (c == second) {
if (first_match < 0) {
first_match = (int)(start + i);
}
last_match = (int)(start + i);
} else if (first_match >= 0) {
break;
}
}
if (first_match < 0 || last_match < first_match) {
return false;
}
*out_start = (uint16_t)first_match;
*out_count = (uint16_t)(last_match - first_match + 1);
return true;
}
const char* mnemonic_word_at(uint16_t index) {
if (index >= 2048) {
return nullptr;

View File

@@ -7,5 +7,7 @@
const char* mnemonic_word_at(uint16_t index);
int mnemonic_word_index(const char* word);
bool mnemonic_letter_range(char letter, uint16_t *out_start, uint16_t *out_count);
bool mnemonic_second_letters(char first, char *out, uint8_t *count);
bool mnemonic_prefix_range(char first, char second, uint16_t *out_start, uint16_t *out_count);
int generate_mnemonic_12(char* out, size_t out_len);
bool mnemonic_validate_basic(const char* mnemonic);

View File

@@ -491,8 +491,8 @@ int socket_name_random(char *out, size_t out_len);
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 32
#define NSIGNER_VERSION "v0.0.32"
#define NSIGNER_VERSION_PATCH 33
#define NSIGNER_VERSION "v0.0.33"
/* NSIGNER_HEADERLESS_DECLS_END */