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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions codex32_terminal/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Python artifacts
__pycache__/
*.py[cod]
*.so
*.egg-info/
dist/
build/

# Virtual environments
venv/
.venv/
env/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Testing
.pytest_cache/
.coverage
htmlcov/

# OS
.DS_Store
Thumbs.db
111 changes: 95 additions & 16 deletions codex32_terminal/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Codex32 Terminal MVP

This folder contains a terminal-based MVP for validating Codex32 shares, recovering a secret `S` share from `k-of-n` shares, and converting the master seed into a 12-word BIP39 mnemonic (display-only).
This folder contains a terminal-based MVP for validating Codex32 shares, recovering a secret `S` share from `k-of-n` shares, and converting the master seed into a BIP39 mnemonic (display-only).

## Supported Seed Sizes

| Seed Size | Codex32 Length | BIP39 Output |
|-----------|----------------|--------------|
| 128-bit | 48 characters | 12 words |
| 256-bit | 74 characters | 24 words |

## Current status (Jan 31, 2026)

Expand All @@ -11,43 +18,102 @@ This folder contains a terminal-based MVP for validating Codex32 shares, recover
- Checksum + header validation
- `k-of-n` share recovery via interpolation
- Display recovered `S` share, seed hex, and BIP39 mnemonic
- **128-bit and 256-bit seed support** (auto-detected)

⚠️ Not an ECW yet (no error correction). The codex32 Python library does **not** provide substitution/erasure correction, so this tool does not attempt it. Error-correction should be implemented separately before advertising ECW behavior.

## Setup (Windows PowerShell)
## Setup

### macOS / Linux

```bash
cd codex32_terminal
python3 -m venv venv
source venv/bin/activate
pip install codex32 embit
```

### Windows PowerShell

From the repo root:

```powershell
python -m venv .\codex32_terminal\venv
.\codex32_terminal\venv\Scripts\Activate.ps1 ; pip install codex32 embit
pip freeze > .\codex32_terminal\requirements.txt
```

> Note: Use a semicolon between `Activate.ps1` and subsequent commands in PowerShell.

## Run

### Box-by-box entry (default)

**macOS / Linux:**
```bash
source venv/bin/activate
python src/main.py
```

**Windows PowerShell:**
```powershell
.\codex32_terminal\venv\Scripts\Activate.ps1 ; python .\codex32_terminal\src\main.py
.\venv\Scripts\Activate.ps1 ; python src\main.py
```

Features:

- Prefix `MS1` is pre-filled.
- Enter one character per box.
- Backspace: press Enter on empty input or type `<` to go back.
- Ctrl+C cancels entry.
- Prompts for seed size (128-bit or 256-bit) at start
- Prefix `MS1` is pre-filled
- Enter one character per box
- Backspace: press Enter on empty input or type `<` to go back
- Ctrl+C cancels entry

### Full-share paste mode

**macOS / Linux:**
```bash
source venv/bin/activate
python src/main.py --full
```

**Windows PowerShell:**
```powershell
.\codex32_terminal\venv\Scripts\Activate.ps1 ; python .\codex32_terminal\src\main.py --full
.\venv\Scripts\Activate.ps1 ; python src\main.py --full
```

Paste full shares in sequence. For `k-of-n` shares, the tool will ask for additional shares until the threshold is met.
Paste full shares in sequence. Seed size is auto-detected from string length (48 or 74 chars). For `k-of-n` shares, the tool will ask for additional shares until the threshold is met.

## Run Tests

**macOS / Linux:**
```bash
source venv/bin/activate
python tests/test_vectors.py
python tests/test_256bit.py
```

**Windows PowerShell:**
```powershell
.\venv\Scripts\Activate.ps1
python tests\test_vectors.py
python tests\test_256bit.py
```

### Expected test output

```
vector2: seed OK -> spice afford liquid stool forest agent choose draw clinic cram obvious enough
vector3: seed OK -> zoo ivory industry jar praise service talk skirt during october lounge absurd
test_valid_lengths_constant: PASS
test_256bit_parse: PASS
test_256bit_validate_s_share: PASS
test_256bit_seed_extraction: PASS
test_256bit_to_mnemonic: PASS (24 words)
Mnemonic: zoo ivory industry jar praise service talk skirt during october lounge acid year humble cream inspire office dry sunset pride drip much dune arm
test_128bit_still_works: PASS (12 words)
test_auto_detect_length: PASS
test_invalid_length_rejected: PASS
test_seed_bytes_to_mnemonic_both_sizes: PASS

All 256-bit tests passed!
```

## Test vectors

Expand All @@ -64,7 +130,7 @@ Expected output:
- Seed hex: `d1808e096b35b209ca12132b264662a5`
- BIP39 mnemonic: `spice afford liquid stool forest agent choose draw clinic cram obvious enough`

### Vector 3 (k=3, cash)
### Vector 3 (k=3, cash, 128-bit)

- Share a: `ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t`
- Share c: `ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr`
Expand All @@ -74,7 +140,16 @@ Expected output:

- Recovered S-share: `ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln`
- Seed hex: `ffeeddccbbaa99887766554433221100`
- BIP39 mnemonic: `zoo ivory industry jar praise service talk skirt during october lounge absurd`
- BIP39 mnemonic (12 words): `zoo ivory industry jar praise service talk skirt during october lounge absurd`

### Vector 4 (256-bit seed)

- S-share: `ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma`

Expected output:

- Seed hex: `ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100`
- BIP39 mnemonic (24 words): `zoo ivory industry jar praise service talk skirt during october lounge acid year humble cream inspire office dry sunset pride drip much dune arm`

## Codebase overview

Expand All @@ -94,13 +169,17 @@ Expected output:
- Progress display, preview/confirm, prompts

- `tests/test_vectors.py`
- Manual harness for BIP-93 vectors 2/3
- Manual harness for BIP-93 vectors 2/3 (128-bit)

- `tests/test_256bit.py`
- Tests for 256-bit seed support (BIP-93 vector 4)
- Verifies both 128-bit and 256-bit paths work correctly

### Implementation rationale

- **Validation** uses `codex32.Codex32String`, which enforces checksum + header correctness.
- **Recovery** uses `Codex32String.interpolate_at` to reconstruct the `S` share from `k` valid shares.
- **BIP39 mnemonic** is a display encoding of the 16-byte master seed (no PBKDF2). This mirrors BIP-93 guidance.
- **BIP39 mnemonic** is a display encoding of the master seed (no PBKDF2). 128-bit seeds produce 12 words, 256-bit seeds produce 24 words. This mirrors BIP-93 guidance.

## Next steps (SeedSigner port)

Expand Down
34 changes: 24 additions & 10 deletions codex32_terminal/src/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
)


TOTAL_LEN = 48
# Supported seed sizes - must match VALID_LENGTHS in model.py
# 128-bit (48 chars) or 256-bit (74 chars)
LEN_128BIT = 48
LEN_256BIT = 74
BASE_PREFIX = "MS1"
FIRST_BOX = len(BASE_PREFIX) + 1
CANCELLED = object()
Expand All @@ -34,18 +37,18 @@ def _is_backspace(value: str) -> bool:
return value == "" or value == "<"


def collect_codex32_boxes(prefix: str, start_box: int) -> str:
def collect_codex32_boxes(prefix: str, start_box: int, total_len: int) -> str:
current = prefix
view.display_progress(current, TOTAL_LEN)
view.display_progress(current, total_len)
box_number = start_box
while box_number <= TOTAL_LEN:
while box_number <= total_len:
raw = view.get_box_input(box_number)
ch = _normalize_box_char(raw)
if _is_backspace(ch):
if len(current) > len(prefix):
current = current[:-1]
box_number -= 1
view.display_progress(current, TOTAL_LEN)
view.display_progress(current, total_len)
else:
view.display_error("Already at the first editable box.")
continue
Expand All @@ -56,7 +59,7 @@ def collect_codex32_boxes(prefix: str, start_box: int) -> str:
view.display_error("Invalid bech32 character. Use bech32 charset.")
continue
current += ch
view.display_progress(current, TOTAL_LEN)
view.display_progress(current, total_len)
box_number += 1
return current

Expand All @@ -66,10 +69,10 @@ def _display_and_confirm(codex_str: str) -> bool:
return view.confirm("Submit this codex32 string?")


def _collect_share_box(prefix: str, start_box: int, index: int, total: int) -> str | object | None:
def _collect_share_box(prefix: str, start_box: int, index: int, total: int, total_len: int) -> str | object | None:
view.display_share_prompt(index, total)
try:
codex_str = collect_codex32_boxes(prefix, start_box)
codex_str = collect_codex32_boxes(prefix, start_box, total_len)
except KeyboardInterrupt:
view.display_cancelled()
return CANCELLED
Expand Down Expand Up @@ -99,11 +102,19 @@ def _collect_share_full(prefix: str | None, index: int, total: int) -> str | obj

def run(entry_mode: str = "box") -> int:
view.display_welcome(entry_mode)

# For box mode, ask user about seed size
total_len = LEN_128BIT # default
if entry_mode == "box":
seed_size = view.get_seed_size_choice()
if seed_size == "256":
total_len = LEN_256BIT

while True:
if entry_mode == "full":
result = _collect_share_full(BASE_PREFIX, 1, 1)
else:
result = _collect_share_box(BASE_PREFIX, FIRST_BOX, 1, 1)
result = _collect_share_box(BASE_PREFIX, FIRST_BOX, 1, 1, total_len)
if result is CANCELLED:
return 1
if result is None:
Expand Down Expand Up @@ -142,12 +153,15 @@ def run(entry_mode: str = "box") -> int:
if first_share.s.isupper():
prefix = prefix.upper()

# For subsequent shares, use the same total_len as detected from first share
share_total_len = len(first_share.s)

while len(shares) < threshold:
share_index = len(shares) + 1
if entry_mode == "full":
result = _collect_share_full(prefix, share_index, threshold)
else:
result = _collect_share_box(prefix, len(prefix) + 1, share_index, threshold)
result = _collect_share_box(prefix, len(prefix) + 1, share_index, threshold, share_total_len)
if result is CANCELLED:
return 1
if result is None:
Expand Down
Loading