Skip to content

Assemble the recovery tool from the same crypto core as the app - #47

Closed
deanrie wants to merge 2 commits into
seQRets:mainfrom
deanrie:refactor/recovery-from-crypto-core
Closed

deanrie wants to merge 2 commits into
seQRets:mainfrom
deanrie:refactor/recovery-from-crypto-core

Conversation

@deanrie

@deanrie deanrie commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #43 — this branch includes that PR's commit (the NFC normalization is what made the duplication bite). The diff specific to this PR is the last commit. Merge #43 first, or I can rebase onto main.

Why

site/ittybitz-recovery.html carried a hand-maintained copy of the decrypt core. The regression suite caught drift — that's what it's for — but every change to the container format or key derivation still had to be made twice, and #43 was exactly that: the same fix applied to crypto-core.js and to the recovery file by hand.

What

One source for the core, two shipped files assembled from it:

scripts/build/crypto-core.js      format + key derivation + decrypt   → both files
scripts/build/crypto-encrypt.js   ittybitzEncrypt                     → app only
scripts/build/recovery-head.html  the recovery page (HTML + CSS)
scripts/build/recovery-app.js     the recovery UI wiring

build-app.mjs assembles site/index.html as before — crypto-core.js + crypto-encrypt.js inside <script id="ittybitz-crypto-core">, so the regression suite's extraction is unchanged — and now also assembles the recovery tool as recovery-head.html + crypto-core.js (<script id="ittybitz-decrypt-core">) + recovery-app.js. It refuses to build if crypto-core.js defines ittybitzEncrypt, so the recovery tool stays decrypt-only by construction, not by discipline.

Behavioural differences in the recovery tool's core, both for the better: it now validates the password like the app does (length, NUL), and its "newer version" message is the shared one. Its UI is untouched.

What is tested is still the shipped file: the suite extracts the block from the HTML and replays every fixture through it, so a build that assembled the wrong thing fails. With #45's reproducibility gate, CI will also fail if the committed recovery file is not what the build produces.

Verified

  • npm run build twice → byte-identical; npm run test:crypto → 101/101 (all 33 fixtures through the assembled recovery core, both NFC spellings, the v3.0.11 NFD fixture).
  • Served the assembled recovery tool and decrypted a real v2.7.3 fixture through its UI: correct plaintext, blurred by default. (A stale CSP pin would have made the page do nothing.)
  • README (build section) and the suite's comments updated; Recover/README.md's user-facing "no build step" claims remain true — nothing changes for someone opening the file.

The same visible password can arrive as different code points: "é" is
U+00E9 (NFC) on most keyboards but "e"+U+0301 (NFD) from some macOS
inputs, IMEs and pasted text. TextEncoder turns those into different
UTF-8 bytes, so PBKDF2 derived a different key and a file encrypted on
one machine failed on another with a misleading "wrong password".

Encryption now NFC-normalizes the password in all three implementations
(src/lib/crypto.ts, the app's crypto core, the recovery tool). Decryption
tries NFC first and, only when the typed string was not already NFC,
retries with the exact typed bytes — which is how every pre-existing
ciphertext was keyed. Nothing that decrypted before stops decrypting;
the only cost is one extra PBKDF2 run for a wrong non-NFC password.

Regression suite:
- new fixture "nfd-password": produced by the unmodified v3.0.11
  crypto.ts with an NFD password, kept as a JSON \u0301 escape so no
  editor can recompose it; proves the exact-bytes fallback.
- fixtures may now carry their own "password".
- NFC<->NFD cross checks for crypto.ts, the app core and the recovery
  file (6 checks that fail against the previous code, pass now).

site/index.html, site/ittybitz-recovery.html and SHA256SUMS.txt are the
rebuilt artifacts with re-pinned CSP hashes (npm run build).
site/ittybitz-recovery.html carried a hand-maintained copy of the
decrypt core. The regression suite caught drift, but every change to the
container format or key derivation had to be made twice (the NFC
normalization was the latest). Now there is one source:

  scripts/build/crypto-core.js     format + key derivation + decrypt
  scripts/build/crypto-encrypt.js  encrypt, appended for the app only

build-app.mjs assembles site/index.html as before (the two files above
inside <script id="ittybitz-crypto-core">, so the regression suite's
extraction is unchanged) and now also assembles the recovery tool from
recovery-head.html + crypto-core.js (<script id="ittybitz-decrypt-core">)
+ recovery-app.js. It refuses to build if crypto-core.js defines
ittybitzEncrypt, so the recovery tool stays decrypt-only by construction.

What is tested is still the SHIPPED file: the suite extracts the block
from the HTML and replays every fixture through it. 101/101. A second
build is byte-identical. README and the suite's comments updated.
@deanrie

deanrie commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Also available merged with every other review PR, in dependency order and verified together, as #57 — merge that or the individual PRs, not both.

@seQRets

seQRets commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Thank you, Dean, for all the work and support you've put into IttyBitz. This review was thorough and genuinely useful.

I'm closing this one without merging. I'm deeply committed to password security, and I'm keeping the core cryptography exactly as it is. I'm also determined to keep the file as small and tight as possible.

I love the changes you've contributed, and several of them are already live in v3.0.12: several files at once, the key-file fingerprint, the printable emergency card, refusing empty key files, keyboard tabs and the accessibility fixes, and the CI reproducibility check. They're credited in the release notes.

Please keep the suggestions coming!

@seQRets seQRets closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants