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
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ checksummed, secret-sharing-aware Base32 format for Bitcoin master seeds. A
master seed is the private recovery secret from which a Bitcoin wallet derives
its keys.

This project provides a command-line tool and Python library that can:
This project provides a command-line tool, an optional graphical user interface, and a Python
library that can:
- create an unshared master-seed backup or an M-of-N shared backup;
- check backup text and suggest possible repairs after damage;
- recover a master seed from the required shares;
Expand All @@ -16,10 +17,9 @@ This project provides a command-line tool and Python library that can:
With an M-of-N backup, any M of the N paper shares can recover the master seed.
A set with fewer than M shares cannot recover it.

This is not a Bitcoin wallet. It has no graphical interface, cannot show
balances or send bitcoin, and does not produce BIP39 mnemonic words. codex32
makes no network connection; it communicates with a local Bitcoin Core
instance through `bitcoin-cli`.
This is not a Bitcoin wallet. It cannot show balances or send bitcoin, and does
not produce BIP39 mnemonic words. codex32 makes no network connection; it
communicates with a local Bitcoin Core instance through `bitcoin-cli`.

This is security-critical reference software. Use it on a trusted computer and
obtain an independent review before relying on it with funds. See
Expand Down Expand Up @@ -54,6 +54,37 @@ BIP39 worksheet profiles are supported for existing-backup recovery but are
Powerful correction searches, including recovery of genuinely unreadable
characters, require interactive confirmation.

### The optional GUI

`codex32-gui` does the same master-seed jobs in a graphical interface, for someone
who has never used codex32 before. It adds no Python dependency: GTK 4 and libadwaita
arrive as system packages. If your system does not already provide them, install
them first:

```bash
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1
```

Then create the environment with `--system-site-packages` so that it can see
those packages:

```bash
python -m venv --system-site-packages .venv
source .venv/bin/activate

python -m pip install --require-hashes \
-r requirements/cli-build-dependencies.txt
python -m pip install --no-build-isolation --no-deps '.[gui]'
python -m pip check
codex32-gui
```

Because this environment can see system Python packages, `pip check` may also
report pre-existing problems in unrelated applications. Those packages are not
GUI dependencies. Tails 7 and Debian 13 already carry the three
required system packages, so an amnesic session needs no download. See the
[GUI guide](docs/user/gui.md).

## Start here

Start Bitcoin Core 32 or newer with local RPC enabled. codex32 detects and
Expand Down
10 changes: 10 additions & 0 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ blank and comment-only lines while counting subpackages recursively. Changing
the budget requires explicit review and authorization together with the matching
documentation and enforcement update.

### The optional graphical package

`src/codex32_gui/` is a second distribution package in this repository,
installed as `codex32[gui]` and started by `codex32-gui`. It is a client of the
surface above and of the private Core adapter; nothing in `src/codex32/` imports
it, and the base install keeps its property of having no third-party runtime
dependency. It carries its own budget of 2,000 logical review lines, separate
from the 5,200 above. Its own boundaries are documented in
[`gui.md`](gui.md) and enforced by `tests/test_gui_boundaries.py`.

## Profile and opaque-HRP capabilities

There is no runtime registration. An unknown HRP uses generic codex32 rules and
Expand Down
125 changes: 125 additions & 0 deletions docs/developer/gui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Reviewing the graphical program

`src/codex32_gui/` is an optional package installed with `codex32[gui]` and run
as `codex32-gui`. It adds presentation and Bitcoin Core orchestration; it adds no
cryptography, entropy source, socket, or file storage.

## Review order

| File | Purpose |
|---|---|
| `reading.py` | Entry interpretation and repair policy; no GTK. |
| `wallet_setup.py` | All Bitcoin Core calls, passphrase handling, and relocking; no GTK. |
| `work.py` | One bounded background operation at a time. |
| `entry.py` | Ordinary, correction, and read-back entry modes. |
| `pages.py` | Screen construction and wording. |
| `app.py` | Application and window setup. |
| `style.py` | CSS string. |
| `__init__.py`, `__main__.py` | GTK setup and entry point. |

Review `reading.py`, `wallet_setup.py`, and `work.py` first. Their behavior is
covered without a display. `tools/gui_walkthrough.py` exercises the real GTK
screens under Xvfb. `tests/test_gui_boundaries.py` enforces a separate 2,000
logical-line GUI budget.

## Security boundaries

`tests/test_gui_boundaries.py` checks the first four mechanically.

1. **No entropy or cryptography.** Seed creation stays in `CreationCeremony`.
2. **No network stack.** The GUI imports no socket, TLS, HTTP, or subprocess
modules. Bitcoin Core access remains in the library's `bitcoin-cli` wrapper.
3. **No secret persistence.** The GUI imports no filesystem, database, logging,
or clipboard storage APIs and never calls `open`.
4. **One Core boundary.** Only `wallet_setup.py` imports
`codex32._bitcoin_core`.
5. **Page secrets are cleared.** `_forget_when_gone` clears card or entry text
when an `AdwNavigationView` page leaves the stack.
6. **One worker at a time.** `work.run` serializes background jobs, returns on the
GTK thread, and drops callbacks for pages that are gone.
7. **Accessibility is opt-in.** `__init__.py` defaults `GTK_A11Y` to `none`
before GTK loads; an operator can override it.
8. **Labels never select wallets.** Choice rows use position and disable markup;
exact wallet-name confirmation remains in the Core adapter.

## Entry and correction

The entry widget has three internal modes:

- **Ordinary:** supplies and protects `MS1` and blocks forward typing after an
invalid header. Multi-card jobs may use `?` for an unreadable header character;
Check a card does not accept `?`.
- **Correction:** preserves damaged headers and literal `?`, matching the CLI's
correction input.
- **Read-back:** starts empty, normalizes only spacing and ASCII case, and never
reveals expected text after a mismatch.

Enter activates the enabled primary action. Card displays use four
four-character groups per row, matching `docs/user/recovery-card.html`.
Ordinary repair suggestions are unmarked; explicit correction may mark changed
groups, which are described as changes rather than known error locations.

Low-discrimination correction keeps the CLI's literal-`YES` gate. Its single
warning covers both hazards: checksum completion can lock errors into new data,
and a damaged-card guess can be wrong. It also forbids replacing a checksum just
to make invalid data validate.

## Bitcoin Core integration

These GUI-specific behaviors are confined to `wallet_setup.py` and documented in
`docs/security/invariants.md` and `docs/security/model.md`.

**Passphrase input.** The GUI may ask for a wallet passphrase. It reaches
`bitcoin-cli` through `-stdinwalletpassphrase`, never `argv`, and is not stored.
The operator can instead unlock in Bitcoin Core.

**Relocking.** `wallet_setup.fill` owns unlock, import, and relock in one
`finally`, including failures before `BitcoinCore.initialize` has armed its own
relock path. Core's 180-second timeout remains a backstop.

**Blank-wallet creation.** The GUI sends a fixed `createwallet` shape:
`wallet_name`, `disable_private_keys=false`, `blank=true`, plus `passphrase` when
provided. A blank encrypted wallet starts locked, then the GUI unlocks it for
the import.

### Driving `BitcoinCore.initialize`

`wallet_setup._Answer` only answers choices already made by the operator:

- `[y/N]` becomes `y` only for the exact chosen quoted wallet name;
- a numbered prompt gets the number assigned to that exact name by the library;
- anything else raises `Offer` and returns the library's choices to the GUI.

Unexpected or stale state therefore fails closed rather than selecting another
wallet. `_Answer.tell` also rejects terminal-only “press Ctrl-C” waits.

## Differences from `ms32`

- GUI repair does not require Bitcoin Core. If `_best` leaves a tie, the GUI
reports ambiguity instead of using the CLI's fingerprint tie-breaker.
- The GUI can create a blank Bitcoin Core wallet; `ms32 create` does not.
- The GUI uses account 0. Other accounts require `ms32 wallet --account N`.
- The GUI has no checksum-completer action. The low-discrimination route is
still reachable through correction and uses the same warning gate.
- Restore asks the operator to compare wallet identity with the existing record
and does not show a new creation date.
- A corrected card is never reported as intact.
- `xprv`, Core Lightning, BIP39 worksheet profiles, and the generic `codex32`
façade remain command-line only.

## Known limits

- Long cards scroll horizontally because entry uses one `Gtk.Entry`.
- Empty-wallet lists refresh on request, not continuously.
- Widget construction is covered by `tools/gui_walkthrough.py`, not pytest.
- On X11, selecting entry text briefly exposes it through the primary selection.
- Enabling GTK accessibility exposes GUI text to other processes on that bus.
- A running spinner page cannot be left; correction and `bitcoin-cli` operations
are bounded by their configured deadlines.

## Desktop identity and artwork

The six home actions use six crops from the MIT-licensed Codex32 book cover.
`artwork/LICENSE` carries the attribution and source. The Codex32 orb is also
installed as the `io.github.benwestgate.codex32` launcher icon beside the matching
desktop file. Packaging tests verify both assets are present.
15 changes: 13 additions & 2 deletions docs/security/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ and evidence.
operator confirms an eligible descriptor wallet by exact name.
8. Wallet state is revalidated before import. Every import must succeed and the
exact accepted public descriptor set must match.
9. codex32 has no passphrase channel. An unlocked encrypted signer is relocked
and verified on every exit path.
9. The installed library and both command-line programs have no passphrase
channel. The graphical program declares one exception, confined to
`codex32_gui/wallet_setup.py`, which is also the only module there that
speaks to Bitcoin Core: it may send an operator-supplied passphrase to
`bitcoin-cli` on standard input to unlock a wallet, may create one blank
descriptor wallet with a fixed set of arguments and no options, and may
request `walletlock`. It stores no passphrase and writes nothing to disk.
An unlocked encrypted signer is relocked and verified on every exit path: by
the library, unchanged, and additionally by a `finally`-protected obligation
covering every wallet the graphical program itself unlocked. No window may
close out of that obligation. The graphical program also disables the
toolkit's accessibility bus before the toolkit starts, unless the operator
has set it themselves.
10. External text, Core output, public wallet data, and PSBTs are untrusted.
11. Only Bitcoin Core descriptor wallets sign with codex32-derived keys.
Sensitive operations use only codex32 or Core on malware-free computers
Expand Down
57 changes: 53 additions & 4 deletions docs/security/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,12 @@ The operator must:
default identifier; fingerprints are metadata, not secrets.
- Private Bitcoin Core descriptors contain the root xprv and temporarily exist
in Python objects, serialized JSON, and the child process's standard input.
- Wallet encryption belongs to Bitcoin Core. codex32 accepts an eligible
unencrypted or unlocked encrypted wallet and never evaluates or handles a
passphrase.
- Wallet encryption belongs to Bitcoin Core. The library and both command-line
programs accept an eligible unencrypted or unlocked encrypted wallet and never
evaluate or handle a passphrase. The graphical program does handle one; see
the graphical program below. Neither can evaluate passphrase strength, and
neither can prevent a passphrase from remaining in Python objects or in the
child process's standard input buffer.
- A malicious or failing `bitcoin-cli`, Core instance, configuration, or host
can violate the destination boundary. Process termination, power loss, or a
Core failure can prevent application cleanup; codex32 reports when locking
Expand Down Expand Up @@ -231,7 +234,7 @@ signing setup belong to Bitcoin Core's maintained v32 workflow.
| Process boundary | codex32 invokes the reviewed `bitcoin-cli` from `PATH` as a child without a shell, direct RPC socket, wallet database, or wallet-creation operation. Every call uses loopback and the selected chain. |
| Destination | Only an empty descriptor wallet with private keys enabled, no external signer, transactions, descriptors, keypool entries, or active scan is eligible. One eligible wallet is offered directly; multiple wallets are selected by number. New wallets are detected by polling, and rejection returns to every eligible wallet. The escaped name is confirmed exactly. |
| Seed source | The original ceremony result or validated recovered master seed supplies root-xprv private descriptors for Core's reported chain. After import, Core v32's wallet HD-key RPCs derive the requested BIP44, BIP49, BIP84, and BIP86 account xpubs. |
| Secret channel | Private descriptor JSON is sent only through the child's standard input. It is absent from arguments, ordinary output, and diagnostics. codex32 has no passphrase channel and suppresses raw Core errors. |
| Secret channel | Private descriptor JSON is sent only through the child's standard input. It is absent from arguments, ordinary output, and diagnostics. The library and the command-line programs have no passphrase channel, and raw Core errors are suppressed. |
| Revalidation | Every destination property is checked again immediately before import. Every private import must succeed before public verification begins. `gethdkeys` must expose one private wallet root; `derivehdkey` must return the requested hardened account paths with one consistent fingerprint and the correct network xpub/tpub version. `getdescriptorinfo` then validates and expands the fixed public templates, and the exact eight active descriptors must match Core's accepted set. |
| Relocking | Once Core reports an encrypted private-key wallet unlocked, a `finally`-protected obligation requests `walletlock` and verifies the locked state after success, failure, state change, or interruption. |

Expand All @@ -245,10 +248,56 @@ and escaped for presentation; they never become shell syntax. A failure after
share-string confirmation leaves valid shares but an incomplete wallet
initialization.

### The graphical program

`codex32-gui` shares every control above, because import and revalidation remain
`BitcoinCore.initialize`. It declares three departures, all confined to
`codex32_gui/wallet_setup.py`, the only module in that package that imports the
Core adapter.

| Departure | Required behavior |
|---|---|
| Passphrase | The operator may supply a Bitcoin Core wallet passphrase. It reaches `bitcoin-cli` through `-stdinwalletpassphrase`, never through an argument, so it is absent from `/proc` and process listings. It is not stored, not logged, and not written to disk, and a passphrase containing a line break is refused rather than truncated. A passphrase this computer's locale would encode as something other than what Bitcoin-Qt sends is refused, so no half-encoded secret reaches a screen or a traceback. The screen keeps the command line's behavior as an alternative: the operator may unlock in Bitcoin-Qt instead, and the program then only rechecks wallet state. |
| Wallet creation | `createwallet` may be issued once, with `wallet_name`, `disable_private_keys=false`, `blank=true`, and a `passphrase` only when one was given. No other option is sent, and the resulting wallet must pass the same eligibility test as any other destination before it is used. Names are restricted to printable text without leading or trailing spaces, and may not contain a slash or be `.` or `..`, so a name can neither span the one-argument-per-line channel nor describe a path. |
| Relocking | Every wallet this program unlocks carries a `finally`-protected obligation of its own, in `wallet_setup.fill`, that requests `walletlock` and verifies `unlocked_until` is zero. The library's obligation is armed only after it has chosen a wallet, so a refusal raised before that point would otherwise leave an unlocked wallet open until Bitcoin Core's own timeout. Worker threads are not daemons, so closing the window during an import runs both obligations rather than skipping them. |

Destination selection is unchanged and is not delegated to prompt wording. The
program answers the library's selection prompts only for a name the operator
already chose on screen, confirms that exact name when the library asks again,
and otherwise raises rather than answering, so a stale or unexpected listing can
produce a refusal but never a different wallet. The library's terminal-only
waiting loops are refused for the same reason. The chain the operator chose is
confirmed against the one connected, because the library asks which chain to use
only while more than one answers. On screen a wallet is chosen by the position of
its row, never by the text of its label, and Core's text is rendered without
Pango markup, so a wallet name cannot hide or impersonate another.

The program draws no entropy, opens no socket, starts no process of its own, and
writes no file: no settings, no recent list, no log, and no clipboard write of
recovery text. Entered recovery text is cleared when its screen is left, subject
to the zeroization limitation above.

Two disclosure channels belong to the toolkit rather than to this program, and
are named here because a static import check cannot see either.

- **The accessibility bus.** GTK publishes every label and entry on the desktop's
shared accessibility bus, where any program running as the same user can read
them and can invoke a password entry's own reveal action. The window therefore
sets `GTK_A11Y=none` before GTK starts, in `codex32_gui/__init__.py`, and
leaves the setting alone when the operator has already chosen one, so
`GTK_A11Y=atspi codex32-gui` restores screen-reader support for anyone who
needs it and accepts that exposure.
- **The primary selection.** Selecting text inside the entry field hands it to
the primary selection, which a clipboard manager may copy to disk. The field
takes the selection back on the next main-loop turn, which closes the window
to one turn but does not remove it; an operator who runs a clipboard manager
should not select the text of a card.

## Verification map

| Boundary | Focused evidence |
|---|---|
| Graphical program | [`test_gui_boundaries.py`](../../tests/test_gui_boundaries.py), [`test_gui_reading.py`](../../tests/test_gui_reading.py), and [`test_gui_wallet_setup.py`](../../tests/test_gui_wallet_setup.py) |
| Parsing and profiles | [`test_bech32.py`](../../tests/test_bech32.py), [`test_bip93.py`](../../tests/test_bip93.py), and [`test_profiles.py`](../../tests/test_profiles.py) |
| Creation, sharing, and recovery | [`test_generation.py`](../../tests/test_generation.py), [`test_sharing.py`](../../tests/test_sharing.py), and the BIP93 vectors under `tests/data/` |
| Correction | [`test_correction_bch.py`](../../tests/test_correction_bch.py), [`test_correction_indel.py`](../../tests/test_correction_indel.py), [`correction_capture.py`](../../tools/correction_capture.py), and [`differential_correction.py --verify`](../../tools/differential_correction.py) |
Expand Down
Loading
Loading