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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ avoid comments or tests that restate the implementation. Add or update concise
docstrings when changing public behavior. Write codex32 in lowercase except
when referring to the Codex32 Book.

Keep the installed package below 5,000 logical review lines, as enforced by the
Keep the installed package below 5,025 logical review lines, as enforced by the
existing test. New dependencies, public API signature or return-shape changes,
and lint suppressions require user authorization; an explicit request can
already provide that authorization.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ codex32 strings.

Printable forms:

- [codex32 recovery card](docs/user/recovery-card.html)
- [wallet-verification record](docs/user/wallet-verification-record.html)
- [codex32 recovery card](src/codex32_gui/forms/recovery-card.html)
- [wallet-verification record](src/codex32_gui/forms/wallet-verification-record.html)

## For developers and reviewers

Expand Down
24 changes: 18 additions & 6 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ unsupported but remains in the review scope.

### Size budget

V1 keeps the installed package below 5,000 logical review lines, excluding
V1 keeps the installed package below 5,025 logical review lines, excluding
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.
Expand All @@ -113,8 +113,8 @@ documentation and enforcement update.
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 1,800 logical review lines, separate
from the 5,000 above. Its own boundaries are documented in
dependency. It carries its own budget of 2,050 logical review lines, separate
from the 5,025 above. Its own boundaries are documented in
[`gui.md`](gui.md) and enforced by `tests/test_gui_boundaries.py`.

## Profile and opaque-HRP capabilities
Expand Down Expand Up @@ -200,8 +200,9 @@ emitted and confirmed unchanged, and the original validated artifact initializes
the wallet. This neutral source prompt tries raw hexadecimal first and otherwise
requires a complete explicit `ms1` string; it never infers or corrects a missing
HRP or separator. No entropy is drawn for this path; raw hexadecimal seeds retain
the generation path. Existing imports use timestamp zero to include prior
history. Changing a supplied secret's identifier requires a sharing threshold.
the generation path. Existing imports ask when the seed was first used and
rescan from a day before that date; a blank answer uses timestamp zero to
include all prior history. Changing a supplied secret's identifier requires a sharing threshold.
Shared creation
uses an explicit threshold or full backup header. Without an explicit share
count or indices, thresholds 2 and 3 produce the reviewed 2-of-3 and 3-of-5
Expand Down Expand Up @@ -652,7 +653,18 @@ a stateless root P2PKH descriptor is normalized, `deriveaddresses` derives its
address, and `validateaddress` returns the script hash whose first four bytes are
the BIP32 fingerprint.

The Core calls are fixed: `getnetworkinfo`, `getblockchaininfo`, `listwallets`,
Immediately before `importdescriptors`, `check_history` refuses a dated rescan
Core could not finish: a start within a day of the pruned node's oldest kept
block (`getblockstats`), where a future timestamp counts as the tip's median
time because Core scans from there, or any dated import while `getchainstates`
shows an AssumeUTXO snapshot still being validated in the background. Unclear
answers are refused too. A `"now"` import is never refused: a fresh seed has no
history, and Core shows when its wallets are still catching up. The GUI runs
the same check before `createwallet` for a restore, and `ms32 create
--existing` asks when the seed was first used instead of rescanning from 0.

The Core calls are fixed: `getnetworkinfo`, `getblockchaininfo`,
`getblockstats`, `getchainstates`, `listwallets`,
`getwalletinfo`, `listdescriptors`, `getdescriptorinfo`, `deriveaddresses`,
`validateaddress`, `importdescriptors`, `gethdkeys`, `derivehdkey`, and
`walletlock`. Bitcoin Core alone creates wallets, selects encryption, handles
Expand Down
21 changes: 15 additions & 6 deletions docs/developer/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ without a display and run in ordinary CI. `tools/gui_walkthrough.py` drives the
real widgets through every task under a throwaway X server and is the cheapest
way to see the screens without a desktop.

The package carries its own budget of 2,000 logical lines, separate from the
5,000 the installed library keeps, and `tests/test_gui_boundaries.py` enforces
The package carries its own budget of 2,050 logical lines, separate from the
5,025 the installed library keeps, and `tests/test_gui_boundaries.py` enforces
it. The plan proposed 1,000 before the screens were written and the budget was
1,800 before the security review of 2026-09-19; that review's remediations are
about 250 lines, and the rest of the difference is user-facing wording in
`pages.py`, which is the first priority this program was built for.
`pages.py`, which is the first priority this program was built for. It was
2,000 until the **Before you start** page and handwriting key of 2026-10-03.

## Claims, and how to check each one

Expand All @@ -43,7 +44,10 @@ about 250 lines, and the rest of the difference is user-facing wording in
`hashlib`, or `hmac`. Entropy belongs to `CreationCeremony`.
2. **No network.** Nothing imports `socket`, `ssl`, `urllib`, or `http`, and no
module imports `subprocess`. The only child process is the `bitcoin-cli` the
library already starts.
library already starts. Separately, `_ready_page` may ask the desktop to open
a bundled blank form with `Gtk.FileLauncher`; it is the only caller. It
passes a path under `codex32_gui/forms/`, or under `CODEX32_FORMS_DIR` when a
launcher has copied the forms where a confined browser can read them.
3. **Nothing reaches disk.** Nothing imports `os`, `pathlib`, `io`, `tempfile`,
`shutil`, `pickle`, `sqlite3`, or `logging`, and nothing calls `open`. There
is no settings file, no recent list, no log, and no clipboard write.
Expand Down Expand Up @@ -98,8 +102,13 @@ Bitcoin Core's own 180-second timeout. `unlock` carries the same obligation over
its own post-check. Both are exercised in `tests/test_gui_wallet_setup.py`.

**Creating the blank wallet.** `createwallet` is issued with one fixed shape:
`wallet_name`, `disable_private_keys=false`, `blank=true`, and a `passphrase`
line only when one was given. No other option is ever sent. Bitcoin Core's
`wallet_name`, `disable_private_keys=false`, `blank=true`,
`load_on_startup=true`, and a `passphrase` line only when one was given. No
other option is ever sent. `load_on_startup` has Bitcoin Core record the wallet
in its own settings, so Core loads it at every start and the wallet keeps
processing new blocks. `create` returns Core's warnings, including a failed
startup-setting save, for display on the finished page. Core owns this policy;
the GUI does not maintain a second wallet-loading mechanism. Bitcoin Core's
`CreateWallet` guards its unlock branch with `if (!create_blank)`, so a blank
encrypted wallet is created locked; the window already holds the passphrase, so
it unlocks immediately and the separate unlock screen appears only for a wallet
Expand Down
6 changes: 4 additions & 2 deletions docs/security/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ and evidence.
`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.
descriptor wallet with a fixed set of arguments and no options (one of them,
`load_on_startup=true`, has Bitcoin Core itself keep the wallet loaded at
every start), 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
Expand Down
9 changes: 7 additions & 2 deletions docs/security/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ 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. |
| Wallet creation | `createwallet` may be issued once, with `wallet_name`, `disable_private_keys=false`, `blank=true`, `load_on_startup=true`, and a `passphrase` only when one was given. No other option is sent. `load_on_startup` makes Bitcoin Core, not codex32, persist startup loading in its own settings so the wallet can follow new blocks after restart. Core creation warnings, including a failed startup-setting save, are displayed to the operator on the finished page; codex32 adds no second startup-loading or quarantine policy. 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
Expand All @@ -278,7 +278,12 @@ fingerprint, the identifier result, and the warning before the operator chooses.

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
recovery text. One button pair is the exception to "no process": **Before you
start** can ask the desktop, through GTK's `FileLauncher`, to open one of the two
blank printable forms shipped in `codex32_gui/forms/`. The desktop chooses and
starts the viewer. Only those forms are passed, never recovery text, and this
happens before any seed is drawn. A launcher may set `CODEX32_FORMS_DIR` to a
copy of the forms that a confined browser can read; Bails does this on Tails. 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
Expand Down
38 changes: 32 additions & 6 deletions docs/user/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,23 @@ Choose how many cards you want. Three cards where any two recover the wallet is
the recommended shape: one card can be lost, burned or stolen and your bitcoin is
still safe, and one card on its own tells a finder nothing.

Next, **Before you start** asks you to have one blank recovery card per card, a
pen, and one wallet record ready. Its buttons open the printable
[recovery card](../../src/codex32_gui/forms/recovery-card.html) and
[wallet record](../../src/codex32_gui/forms/wallet-verification-record.html)
forms in your browser. Print them blank and fill them in by hand, ideally in
archival ink. Never print a filled-in card: printers and print queues can keep
a copy of what they printed. Press **I have them ready** to see the first card.

Each card is shown once. Copy it onto paper with a pen, then type it back from
the paper with the original off the screen. That catches a slip of the pen now
rather than years from now. If a group does not match, the window says which one;
correct that group and try again, as many times as you like.

When every card is confirmed, the window shows the master fingerprint. Write it
on your [wallet record](wallet-verification-record.html), then press **I wrote it
down**. This is a new wallet ceremony, so there is no pre-existing fingerprint
or descriptor to authenticate against.
on your wallet record, then press **I wrote it down**. This is a new wallet
ceremony, so there is no pre-existing fingerprint or descriptor to authenticate
against.

Next, choose the Bitcoin Core wallet that will hold the
keys. Only empty wallets are offered, so no wallet you already use can be
Expand All @@ -80,15 +88,20 @@ give it a name and a passphrase, and codex32 fills it in and locks it again.
Forgetting that passphrase does not lose your bitcoin. Your cards still recover
the seed. It protects the wallet on this computer.

Finally, copy the wallet details onto your
[wallet record](wallet-verification-record.html) and keep it apart from every
card. The window shows exactly the fields that record asks for.
Finally, copy the wallet details onto your wallet record and keep it apart from
every card. The window shows exactly the fields that record asks for.

A card never contains **B**, **I**, **O** or **1**: those four are left out of
the alphabet precisely because handwriting confuses them with 8, J, L and 0. If
you type one, the window says so and names what the card probably says, rather
than quietly swallowing it.

Some characters that are left in still look alike in handwriting: 5 and S, 6
and G, 2 and Z. While you write, the window asks you to mark them: slash every
0, cross 7 and Z, draw S with a line through it like $, close the loops of 6
and 9, and give G an open, obvious bar. The recovery card form repeats this
key.

## A card that is damaged

Type what you can still read, and `?` for each character you cannot make out.
Expand Down Expand Up @@ -155,6 +168,19 @@ fingerprint and whether the backup identifier was made from the recovered seed,
explains what that can and cannot prove, and restores only if you still choose
to. Check the balance and history before you send money to that wallet.

The same page asks for the approximate creation date from your wallet record.
Bitcoin Core searches for the wallet's transactions from a day before that date,
so a pruned node, which keeps only recent blocks, can still find them. Leave it
blank to search all history. If the node has already pruned blocks the search
would need, the window says so before it changes anything in Bitcoin Core; a
date after the pruned blocks works, and otherwise the node must download the
blockchain again. A node started from an AssumeUTXO snapshot lacks older blocks
until it finishes checking them, so the window asks you to restore after that.
A new wallet has no history to search, so it is made and loaded at once;
Bitcoin Core itself shows that it is still catching up.
New wallets made here are loaded each time Bitcoin Core
starts, so a pruned node keeps them up to date instead of pruning past them.

After the restore, **check** the remaining wallet details against your record
rather than copy them onto it. It shows no creation date on that screen, because the
real one is already on your record and today's would replace it.
Expand Down
12 changes: 9 additions & 3 deletions docs/user/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ You will need:
- Bitcoin Core 32 or newer, with its local RPC server enabled and
`bitcoin-cli` available on `PATH`;
- codex32 installed using the [README instructions](../../README.md#install);
- one blank [codex32 recovery card](recovery-card.html) per secret or share; and
- a separately stored [wallet-verification record](wallet-verification-record.html).
- one blank
[codex32 recovery card](../../src/codex32_gui/forms/recovery-card.html)
per secret or share; and
- a separately stored
[wallet-verification record](../../src/codex32_gui/forms/wallet-verification-record.html).

Before running `create` for a real wallet, have your blank cards, a pen, and
wallet record ready, and choose separate trusted places for shared cards.
Expand Down Expand Up @@ -120,7 +123,10 @@ Already have a complete codex32 `ms` secret? Run `ms32 create --existing` to
write and confirm its recovery card and initialize a Bitcoin Core wallet.
The existing secret is preserved unchanged. To split it into three cards
requiring any two, use `ms32 create 2 --existing` instead. Enter the secret
only when prompted. Bitcoin Core also scans for prior transactions.
only when prompted. codex32 then asks for the date the seed was first used:
Bitcoin Core scans for prior transactions from a day before it, so a pruned
node can still find them. Leave it blank to scan all history, or enter
today's date if the seed has never received bitcoin.

### 3. Make a Bitcoin Core wallet

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ dev = [
where = ["src"]

[tool.setuptools.package-data]
codex32_gui = ["artwork/*.png", "artwork/LICENSE"]
codex32_gui = ["artwork/*.png", "artwork/LICENSE", "forms/*.html"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
Loading
Loading