Repository navigation
Conversation
The shipped artifact is committed but is meant to be a pure function of scripts/build/* and src/lib/bip39.ts. The regression job tested the committed HTML but never checked it matched a fresh build, so a hand edit to site/index.html (or a source edit without 'npm run build') could ship unnoticed. Rebuild in CI and fail on any diff in site/ or SHA256SUMS.txt.
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).
The gate required 24+ characters AND upper, lower, digit and symbol. That rejects a 7-word Diceware passphrase (~90 bits) while passing "Aaaaaaaaaaaaaaaaaaaaaa1!". NIST SP 800-63B recommends length over composition rules. The existing rule is unchanged (and still what Generate produces). A second way to pass is added: 24+ characters made of 6+ different words of 3+ letters, separated by spaces, hyphens or underscores. Repeated words and single-letter 'words' do not count. Hint text, the rejection message and the README are updated to match. site/index.html and SHA256SUMS.txt are the rebuilt artifacts.
… per release, hygiene - SECURITY.md and site/.well-known/security.txt (RFC 9116): both sister projects had them; the one tool here that holds other people's secrets did not. A weekly workflow fails 30 days before security.txt expires. - release.yml: a vX.Y.Z tag publishes ittybitz.html, ittybitz-recovery.html and SHA256SUMS.txt. It refuses unless the tag, package.json and the footer agree, site/ is reproducible from source, this version has left fixtures, and the regression suite is green; signs both HTML files' provenance through Sigstore (verifiable with gh attestation verify, the one check that does not depend on trusting this repository); then fetches the latest-download links to confirm they serve the released bytes. - scripts/add-release-fixtures.mts (npm run fixtures:add / fixtures:check): encrypts two fixed plaintexts, with and without the key file, using the crypto core extracted from the SHIPPED site/index.html and appends them under package.json's version. Idempotent; never touches an existing entry. Fixtures had stopped at v2.7.3; v3.0.11's four are added here. - dependabot.yml: the npm section referred to a Next.js/TypeScript setup that no longer exists (no dependencies, no lockfile); github-actions only now. package.json license: GPL-3.0-only.
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.
…ing on the AES card - Both tablists follow the WAI-ARIA tabs pattern: one tab stop each (roving tabindex), Left/Right/Home/End move between tabs and activate the one landed on; the File/Text panes are tabpanels linked by aria-controls/aria-labelledby. - The key file's bytes get the same best-effort erase the plaintext already got, in the handler's finally. - 'Military-grade encryption' is a marketing phrase the audit history has been steadily removing; the card now says what the page does: authenticated encryption, key derived by 1,000,000 rounds of PBKDF2. site/index.html and SHA256SUMS.txt rebuilt; test:crypto 91/91.
…by crypto.ts The crypto regression suite proves the core; nothing exercised the page around it. scripts/verify-ui.mts drives both shipped files through their real controls in headless Chrome, zero dependencies, with src/lib/crypto.ts running in Node as the independent judge: what the UI encrypts must open under crypto.ts, and crypto.ts ciphertext must decrypt through the UI. Covers: CSP pins recomputed for both files, no unsafe-inline, no network source, SHA256SUMS current; both pages over file:// and http with no violation, exception or request; weak password refused, strong accepted; encrypt → Base64 with an IBTZ v1 header that opens under crypto.ts; password and secret cleared afterwards; decrypt blurred until the eye; wrong password → generic failure, no output; a v1.0 headerless fixture decrypts and its seed is recognised (fingerprint 73c5da0a, SeedQR offered); SeedQR opens blurred as Standard SeedQR, Reveal enables download; Copy arms a 60 s clear that is skipped if anything else was copied; 20 generated passwords all pass the strength rule; no sideways scroll at 320/390/1440; the recovery tool decrypts a real fixture and carries no encrypt function; both pages refuse a cross-origin frame. Every check was confirmed to fail against deliberately broken code: weak password accepted, password left in the field, output unblurred, clipboard clear ignoring a foreign copy, frame guard removed, stale CSP pin. Also run under Linux Chromium via act: 60/60. ui.yml runs it on every push.
… over as a file
An empty key file adds nothing to the key: with 0 bytes appended, the
derived key is the same as with no key file, so a file that LOOKS
protected by a second factor is not. Both pages now refuse it at pick
time (the app also at Encrypt/Decrypt, defensively) and say why.
Decrypting a file's ciphertext pasted into the text box 'succeeded' and
showed replacement marks, which reads as corruption. Both pages now
decode with TextDecoder({fatal:true}); when the bytes are not UTF-8 they
are downloaded as decrypted.bin with a message saying what happened and
to use file mode next time. Nothing is rendered as text in that case.
Verified in the browser on both pages: a 0-byte key file dropped on the
key zone is refused and not picked; a crypto.ts ciphertext of random
bytes in text mode downloads decrypted.bin and shows the message; a
normal text fixture still decrypts afterwards. test:crypto 91/91.
…ng secret prints; reduced motion honoured - The secret-text field had spellcheck=false but not autocomplete, autocapitalize and autocorrect off, which the sister projects set on every secret-bearing field: on iOS, predictive text LEARNS what is typed into a plain textarea, so a seed phrase entered here could surface as a keyboard suggestion later. Both pages' text fields now carry all four. The password field moves from autocomplete=off (ignored for password inputs by every browser) to new-password, which does suppress autofill. - The status line is a live region (role=status, aria-live=polite): a screen reader now hears 'Weak password', 'Decrypted successfully' and 'Decryption failed' instead of silence. Same for the recovery hint. - @media print blanks every field that can hold a secret, the QR and the fingerprints: Print and Save as PDF write the screen to a file. - prefers-reduced-motion: reduce stops the spinner and transitions. Verified in Chrome with print media emulated: #t, #out, #p hidden, status visible; attributes present. test:crypto 91/91.
…ator from the built-in BIP-39 list The password is cleared after encrypting and is the only way back in, so a typo was discovered when the file was needed, months later. Encrypt mode now has a second field: red while it differs, green once it matches, and Encrypt refuses on a mismatch or an empty repeat. Both generators fill both fields, since a generated password was never typed and has no typo to catch; the notice to save it carries that weight. A 32-character random string cannot be remembered or written down reliably. The page already carries the BIP-39 list, and 2048 = 2^11, so eleven raw bits index it with no bias: eight words carry 88 bits at zero cost to the file. Eight is deliberately not a valid mnemonic length, so no wallet accepts the result as a seed, and the notice says so. Accepted by the strength rule from seQRets#44 (6+ distinct words, 24+ chars). Verified in the browser: mismatch and empty repeat each refuse with their own message and encrypt nothing; a match encrypts and clears both fields; ten generated passphrases are 8 BIP-39 words each, all distinct, mirrored into the repeat field, green border; decrypt mode hides both the repeat field and the Passphrase button. test:crypto 91/91.
Closed issue seQRets#4 asked for bulk import. The main drop zone now takes several files (drop or browse, input multiple). Each is processed on its own — its own salt, its own key derivation, its own download under its own name — and a failure in one does not stop the others: the summary names what succeeded and what did not. The desc reads 'N files · size'. A single file behaves exactly as before. A key file picked months later is just a filename. The key zone now shows the first 8 hex characters of the file's SHA-256 beside its name, and the generated key file announces the same fingerprint when it is downloaded, so the right file can be recognised without revealing anything about it. The empty-key-file refusal from seQRets#50 is folded into the same per-file validation. Verified in the browser: three files encrypted with a key file → three downloads; the three ciphertexts plus a junk file decrypted → three correct plaintexts, the junk named as failed, status red; the key fingerprint shown equals SHA-256 of the bytes. test:crypto 91/91.
From the QR of an encrypted text, 'Print card' prints one page: the QR at 8 cm, the Base64 in full, and three steps to decrypt it — open ittybitz.app or the recovery file kept beside the card, scan or type, enter the password (and the key file, named, if one was used). The password is never on it, so the card can sit in a safe or with a notary and be lost or copied without harm. For a safe, a notary or a drawer, a sheet that says how to open it is worth more than a QR alone. Only ENCRYPTED text gets the button: a ciphertext is made to be kept on paper, a decrypted text or a seed never is, and seQRets#51's print rule keeps hiding those. The card is a print-only section filled when the button is pressed and emptied on afterprint (with a fallback timer), so the ciphertext does not linger in the document for a later Save Page As. Verified in Chrome with print media emulated: the page is replaced by the card; after afterprint the card is empty and the body class gone; the button is absent for a decrypted seed's SeedQR. test:crypto 91/91.
… test:ui now measures every text element --faint #6e6e77 coloured the 13px password hint on the app and the '(only if one was used)' label on the recovery tool: 3.92:1 against the card, under the 4.5:1 WCAG AA asks of normal text. #838391 measures 5.2:1 and still reads as the quiet colour it is meant to be. verify-ui.mts gains a contrast audit for both pages: every visible text node against its background composited up the ancestor chain over the page colour, 4.5:1 normal / 3:1 large. Text over a gradient on a close ancestor (the orange buttons: black on orange, ~8:1 by eye) is skipped as unmeasurable this way; body's faint radial glow is not treated as a gradient, or nothing would be measured. 32 + 17 elements measured; with the old token swapped back in, both pages fail on exactly the two elements above. 62/62.
…rce (recovery-head.html)
…epeat-password field and asserts the mismatch refusal Merging seQRets#52 onto seQRets#49 showed two things the PRs could not see alone: the fourth button (Passphrase) pushed the row to 409px at a 320px viewport, and the harness's encrypt flow never filled the new repeat field, so Encrypt refused. Two rows of two under 480px; the harness fills the repeat field when it exists and checks that a mismatch is refused.
…very-head.html)
This was referenced Oct 8, 2026
Merged
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! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The twelve review PRs all touch the built
site/index.html, so merging them one by one means a conflict and annpm run buildafter almost every one. This branch is the alternative: all twelve merged in dependency order, conflicts resolved in the sources only, the artifacts rebuilt once at the end, and everything green. Merge this, or the individual PRs — not both. Each individual PR remains open for review and links here.Contains, in merge order
test:ui→ Contrast: the faint hint colour was under WCAG AA; raised, and test:ui now measures every text element #56 contrast fix + auditWhat the integration surfaced (fixed here, in three small commits)
scrollWidth 409). The PR was only checked at desktop;test:ui's layout check caught it the moment both were on one branch. Under 480px the row now wraps to two rows of two.site/ittybitz-recovery.htmlin Refuse an empty key file; hand binary results of text-mode decryption over as a file #50, Secret field keeps the keyboard out; status announced; nothing secret prints; reduced motion #51 and Contrast: the faint hint colour was under WCAG AA; raised, and test:ui now measures every text element #56 would have been silently lost on the next build. Each is re-applied inscripts/build/recovery-head.html/recovery-app.js(the contrast one was caught bytest:ui; the other two re-applied from their diffs and verified).test:uilearned the repeat-password field (fills it, asserts a mismatch is refused) and gained a two-files-at-once check: both encrypted, both downloaded, both open undercrypto.ts. Now 64 checks.crypto-fixtures.json: Normalize passwords to NFC; decrypt tries NFC then the exact typed form #43 and SECURITY.md, security.txt, release workflow with provenance, fixtures per release #46 both appended v3.0.11 entries — both kept (37 fixtures, 105/105).Verified
npm run buildtwice → byte-identical; committedsite/andSHA256SUMS.txtequal the build output (what CI: verify site/index.html is reproducible from scripts/build #45's gate checks);update-csp-hashes --checkcurrent;fixtures:check5 present for v3.0.11.test:crypto105/105 ·test:ui64/64.act:crypto-regression.yml(with the reproducibility gate) green,ui.ymlgreen under Chromium,release.ymlas av3.0.11tag push green through staging (the hashes differ from the published v3.0.11, as they must — this is a new build).Version string and CHANGELOG are yours; this touches neither.