Skip to content

BoykoNeov/Cryptographer

Repository files navigation

Cryptographer

An interactive cryptography explorer. Type a plaintext + key, watch every intermediate state of every step of every round, then edit the cipher itself — swap the S-box, reorder steps, change the MixColumns matrix — and see the trace re-run in ~200ms.

Built as a learning tool, not a production crypto library.

Status: v0.9.0 + ChaCha20 — N ciphers + M modes is closed, and the box now holds a stream cipher too. Every block cipher (AES ×3, Speck ×2, Serpent ×3, DES, Blowfish, Twofish — eleven cores) runs every mode of operation (ECB, CBC, CTR, CFB, OFB — all four SP 800-38A confidentiality modes), because the modes are written once against a cipher-agnostic contract rather than per cipher. 2D visual editor complete (all 11 slices), full linear-mode pedagogy: every step narrates what it does inline with byte values (per-frame value-prose), every round key renders side-by-side as a labelled ribbon with the consumed K_i outlined, the key schedule that used to collapse into one passthrough frame now surfaces its per-word internals (AES RotWord → SubWord → Rcon → XOR chain; Serpent prekey recurrence + bitsliced S-boxes + IP), and hovering an output byte cell in the linear inspector lights up the input cell(s) that feed it (same-position for SubBytes/AddRoundKey, a gather for ShiftRows, the four same-column cells with GF ×N labels for MixColumns). Cross-mode mirror buttons across every step type with a known encrypt↔decrypt param relationship. Compose-your-own block-cipher modes from the palette. See CHANGELOG.md for the release log and docs/versioning.md for the versioning policy.

What's in the box

Shipped ciphers (all with both encrypt + decrypt paths and FIPS / NIST / paper-vector tests):

Cipher Variants Block Key Mode of operation
AES 128 / 192 / 256 16 B 16 / 24 / 32 B single-block, ECB + CBC + CTR + CFB + OFB (all three variants)
Speck32/64 BE (paper) + LE (NSA reference) 4 B 8 B single-block, ECB + CBC + CTR + CFB + OFB (both conventions)
Serpent 128 / 192 / 256 16 B 16 / 24 / 32 B single-block, ECB + CBC + CTR + CFB + OFB (all three variants)
DES 8 B 8 B (56 effective) single-block, ECB + CBC + CTR + CFB + OFB
Blowfish 8 B 8 B (v1) single-block, ECB + CBC + CTR + CFB + OFB
Twofish 16 B 16 B (v1) single-block, ECB + CBC + CTR + CFB + OFB
ChaCha20 64 B keystream 32 B stream (no mode of operation — it is one)
Salsa20/20 64 B keystream 32 B stream (no mode of operation — it is one)

Padding schemes (every block cipher): PKCS#7, zero-pad, ISO 7816-4, plus a no-pad option for exact-block input. Blowfish and DES pad to their own 8-byte block and Speck to its 4-byte block, not 16 — the padding overlay reads the width from the active cipher. The stream modes (CTR, CFB, OFB) are exempt: they engage no padding at all and accept a message of any length ≥ 1 byte. The stream ciphers — ChaCha20 and Salsa20 — never pad at all.

ChaCha20 is the first stream cipher, and the first cipher with no BlockCipherCore. Every other cipher above is a keyed permutation on a fixed block that a mode of operation repeats over a long message. ChaCha20 is a different kind of object: it generates a keystream from (key, nonce, counter) and the message meets that keystream exactly once, at an XOR. So it needs no mode wrapped around it — it contains its own counter, accepts any message length ≥ 1, and never pads. Its 20 rounds are pure ARX (Addition, Rotation, XOR) over a 4×4 grid of 32-bit words, with no S-box and no lookup table anywhere, which is why it runs in constant time on any CPU and has no cache-timing side channel to be careful about. Verified against RFC 8439 §2.1.1 (quarter round), §2.3.2 (block function), §2.4.2 (encryption) and node:crypto across the block boundary — and the app's defaults are §2.4.2, so selecting ChaCha20 shows a published test vector, deliberately 114 bytes so the short final block is visible on first look.

Salsa20 is ChaCha20's ancestor, and it is in the box to show one specific difference. Same designer, same ARX family, same 4×4 word state, same keystream-⊕-message structure — but where ChaCha20 assembles its state from four contiguous regions (constants, key, counter, nonce), Salsa20 scatters the same material along the matrix's main diagonal, so its state assembly concatenates eight runs rather than four. Regrouping that into neat regions was one of the simplifications Bernstein made when he revised the design, and putting the two assemblies side by side is the clearest way to see it. Salsa20 also splits its 16-byte IV 8/8 rather than 4/12 — a 64-bit counter and a 64-bit nonce, against ChaCha20's 32-bit counter and 96-bit nonce — which is the same trade viewed from the other end. It is the app's first cipher with no live oracle: node:crypto has no salsa20, so every expected value is a pinned literal, anchored by two independent sources that agree — pycryptodome, and a from-spec reference implementation inside the test file that shares no code or execution model with the cipher under test.

Modes of operation cost N ciphers + M modes, not N × M. ECB, CBC, CTR, CFB, and OFB are written once against a BlockCipherCore contract (block/key width, a key schedule, and seed-parameterized forward + inverse bodies) and know nothing about the cipher underneath — so adding a mode gives it to every cipher, and adding a cipher gives it every mode. As of Twofish's core (the last one), every block cipher above runs every mode — the claim fully paid out. Widths are no obstacle either: Speck's 4-byte block and Blowfish's 8-byte block drive exactly the same generic machinery AES's 16-byte block does.

CTR is the claim's clearest payout: it landed as one mode file plus one arithmetic step, with zero changes to the contract — which had been designed against it from the start (hence the forward body exposed on its own, since CTR decryption also encrypts). It is also the strictest test of every core, because it is the only mode that feeds the cipher body the counter rather than the message block, so a core that merely happened to work when seeded from the usual direction fails immediately. And it is honest about being a stream mode: CTR accepts a message of any length ≥ 1, emits ciphertext exactly as long as the plaintext, and engages no padding at all — the final keystream block is visibly trimmed to the short message block, so a whole message shorter than one cipher block is representable.

CFB is the cheapest payout of all — no new step type whatsoever, reusing CTR's keystream leaves and CBC's feedback wiring. It is the mode that sits between the two: like CTR it encrypts something other than the message (a feedback register) and XORs the result, so the forward cipher runs in both directions and no padding is needed; like CBC that register holds the previous ciphertext block, so blocks are serially dependent. That makes it a self-synchronizing stream cipher — corrupt one ciphertext byte and the plaintext recovers after exactly two blocks, a property you can watch happen in the trace. Its encrypt and decrypt specs differ in exactly one wire: which port refills the register (what we emit vs. what arrived).

OFB completes the set, and is best read as CFB with one wire moved. Its register is refilled not from the ciphertext but from the cipher's own previous outputO_j = E(O_{j-1}) — so the keystream depends on nothing but the key and the IV. Three consequences you can watch in the trace: the keystream could have been generated before the plaintext existed; it cannot be jumped into (reaching block n means performing n encryptions in sequence, which is precisely why CTR displaced OFB in practice); and it has zero error propagation — corrupt one ciphertext byte and exactly one plaintext byte changes, the exact opposite of CFB's two damaged blocks. Because the keystream owes nothing to the message, OFB's encrypt and decrypt are the same spec, structurally identical the way CTR's are. Put the three keystream modes side by side and they agree on block 0 (all encrypt the IV) and diverge from block 1 — CTR to IV+1, CFB to the ciphertext, OFB to E(IV).

Shipped hashes (select Hash in the kind dropdown):

Hash Output Block Notes
SHA-256 32 B 64 B Merkle–Damgård; multi-block, KAT-equal vs FIPS 180-4 §A.1 + §A.2 + node:crypto; the first fully port-native primitive
SHA3-256 32 B 136 B rate sponge (Keccak-f[1600], 24 rounds θρπχι), multi-block, KAT-equal vs FIPS 202 + node:crypto; the first non-Merkle–Damgård hash and the foundation for post-quantum work
SHAKE128 editable 168 B rate Keccak XOF — live-editable output length; multi-block, KAT-equal vs FIPS 202 + node:crypto
SHAKE256 editable 136 B rate Keccak XOF — live-editable output length; the higher-strength variant used across the NIST post-quantum standards
cSHAKE128 editable 168 B rate customizable SHAKE (NIST SP 800-185) — live-editable customization string; verified vs pycryptodome + the NIST published samples
cSHAKE256 editable 136 B rate the 256-bit-strength customizable SHAKE; the base construction under KMAC
KMAC128 / 256 editable 168 / 136 B rate the Keccak keyed MAC (NIST SP 800-185) — the first keyed hash; length-committing tag; verified vs pycryptodome + the NIST samples
KMACXOF128 / 256 editable 168 / 136 B rate the extendable-output KMAC variants (arbitrary-length keyed output)

SHA-256 is built entirely from the universal port-native vocabulary (rotate-bits-right, shift-bits-right, xor, add-mod-32, and, not, concat, split-bytes, byte-slice, …) — no SHA-specific executors. Its 64 compression rounds and the message schedule decompose into individually-scrubbable frames, and multi-block messages fold over a port-mode iterate that carries the running hash as its chain. The explorer caps input at 512 bytes to keep the per-byte trace scrubbable (not a SHA-256 limit).

SHA3-256 is a sponge, structurally unlike SHA-2: it absorbs the padded message 136 bytes at a time into a 200-byte state, running the Keccak-f[1600] permutation (24 rounds of θ→ρ→π→χ→ι) after each block, then squeezes the 32-byte digest out of the state. The absorb loop reuses the same port-mode iterate fold as SHA-256's multi-block hashing (the carried chain is the full state). Each round decomposes into named port-native leaves — θ (keccak.theta), ρ (rotate-lanes, the per-lane rotation), π (permute), χ (not/and/xor, the one nonlinear step), ι (keccak.iota, the round constant). SHA3-256 is the honest prerequisite for the NIST post-quantum standards (ML-KEM, ML-DSA, SLH-DSA), which all build on Keccak/SHAKE.

SHAKE128 / SHAKE256 are Keccak's extendable-output functions (XOFs) — the same Keccak-f[1600] permutation and sponge absorb as SHA-3, but the output length is arbitrary and live-editable. The "output bytes" control (shown when a SHAKE is selected) resizes the digest and rebuilds the trace: SHAKE squeezes rate bytes off the state, permutes, squeezes rate more, and repeats until it has enough — so cranking the length up makes new squeeze-permutation blocks appear on the canvas and cranking it down collapses them. The squeeze is honestly unrolled (not looped), matching FIPS 202 Algorithm 8: for n output blocks there are n extractions and n−1 permutations, with no trailing wasted permute. SHAKE is what every NIST post-quantum standard actually consumes.

cSHAKE128 / cSHAKE256 are the customizable SHAKE from NIST SP 800-185 — a SHAKE whose output is bound to a customization string S (and a NIST-reserved function name N), so any change to S domain-separates the XOF into an unrelated instance. Both strings are live-editable text controls; everything cSHAKE adds lives upstream of the sponge as three visible SP 800-185 encoding steps (encode-string, bytepad, right-encode) that build a customization prefix, after which the identical SHAKE absorb + squeeze runs (only the pad's domain byte differs). With N and S both empty, cSHAKE is defined to equal SHAKE — and the trace shows exactly that. cSHAKE is the base construction under KMAC (the Keccak keyed MAC).

KMAC128 / KMAC256 (and the XOF variants KMACXOF128 / 256) are the Keccak keyed MAC from NIST SP 800-185 — the SHA-3 family's native alternative to HMAC, and this app's first keyed hash. KMAC is cSHAKE with the function name fixed to "KMAC", the message wrapped with a length-prefixed key block in front and a right_encode(L) length commitment behind: binding the output length into the hashed input makes the tag length-committing (a 32-byte tag is not a prefix of a 64-byte one). The key is entered in the key field (which now appears for KMAC — no prior hash consumed a key) and flows through the same aux["key"] channel the block ciphers use; you can watch it enter the sponge as its own aligned block. The key length is variable — the key field is the source of truth, so typing a shorter or longer key rebuilds the trace at that length (the encode_string(K) bit-length prefix, the key block, and the declared key input all follow), defaulting to the NIST 32-byte sample. KMACXOF drops the length commitment (right_encode(0)) so the tag can be squeezed to any length. Verified byte-for-byte against the NIST SP 800-185 published samples and pycryptodome.

DES is the project's first Feistel cipher. Its round body is built port-native — a group of split-bytes → E-expand → XOR with K_i → 8 S-boxes → P-permute → xor → concat — with the Feistel swap expressed as the concat argument order (rounds 1..15 swap; round 16 doesn't, the textbook last-round exception that makes the cipher self-inverse under key-reversal). No special branching primitive — the universal-port thesis is that Feistel needs none.

Blowfish is the second Feistel cipher, and the one place the app keeps a step deliberately opaque. Its round body is fully port-native (split-bytes → xor-with-aux(P[i]) → F → xor → concat, the swap again being the concat order; F is ((S0[a]+S1[b]) ⊕ S2[c]) + S3[d] shown as four aux-fed S-box lookups + two add-mod-32 + one xor). But its key schedule runs the cipher on itself 521 times to derive the key-dependent P-array + four S-boxes — a hard data-dependency chain with no legible frame-by-frame decomposition. So the key ⊕ P mixing IS shown (18 real XOR frames — how a variable-length key enters), while the 521-encryption loop is one honest black-box step that publishes the derived P/S into aux. Decryption is the same network with the P-array consumed in reverse. Key fixed at 8 bytes in v1; verified against the Eric-Young / pycryptodome vector set. Blowfish is also the first non-AES cipher with ECB + CBC, and the first whose 8-byte block exercises the mode machinery for real — every earlier core was AES, where a stray hardcoded 16 would have gone unnoticed.

Twofish is the third Feistel cipher — Blowfish's 1998 AES-finalist successor, and the richest cipher in the box. Its round body is port-native (split → g(R0) / g(ROL(R1,8)) → pseudo-Hadamard combine with two subkeys → 1-bit rotations → concat, the swap again the concat order), where g is four aux-fed byte→byte S-box lookups feeding an MDS matrix over a second GF(2⁸) field (0x169, not AES's) via a generalized gf-matrix-multiply@2. The key schedule follows Twofish's partial-visibility split: the pseudo-Hadamard subkey mixing is 20 visible blocks of add / rotate frames, while the h-function machinery (the Reed–Solomon S-vector over a third field 0x14D, the key-dependent S-box construction, and the 40 h-evaluations) is one honest opaque twofish.h-expand@1 step — but, unlike Blowfish's silent monolith, it carries a rich value-prose narrator that discloses its four hidden stages annotated with the real per-key numbers this run produced (per user request). Words travel the ports big-endian to reuse the generic ARX primitives; the little-endian crossing is localized to visible permute reversals. Key fixed at 128 bits in v1; verified at three levels — S-boxes, all 40 subkeys, and endpoint ciphertext — against Niels Ferguson's reference implementation and the published spec's constants, agreeing on the canonical all-zero vector 9f589f5c…c35a.

Shipped public-key algorithm (select Public-key in the kind dropdown):

Algorithm Key material Notes
RSA (textbook) editable p, q, e constants traced key generation + square-and-multiply encrypt/decrypt; KAT-verified against a Python pow() oracle

RSA is the project's first public-key cipher — no symmetric key, big-integer modular arithmetic instead of byte permutation. Both halves of the "magic" are visible frames: key generation derives n = p·q, φ(n) = (p-1)(q-1), and the private exponent d = e⁻¹ mod φ as a traced extended-Euclid loop (one frame per division step — the (r, newR, t, newT) tuple shifting toward gcd and the inverse, the Bézout coefficient shown reduced mod φ to keep every value a non-negative integer), and exponentiation is an unrolled square-and-multiply ladder (c = mᵉ mod n / m = cᵈ mod n) built from mul / sub / mod-mul / cond-mod-mul / eea-step / eea-extract primitives whose bigint math lives inside the executor and exchanges Uint8Array at every port. The exponent is live-editable — each ladder rung reads its bit at run time, so editing e (or p, qd) re-runs the trace. Textbook sizes (default p=61, q=53, e=17, n=3233); the message m must satisfy 0 ≤ m < n.

Interactive features:

  • Per-frame value-prose narration — every step in every cipher emits a collapsible <details> block per conceptual sub-unit (SubBytes → 16 byte units, MixColumns → 4 GF(2^8) dot-product breakdowns, AddRoundKey → 16 XOR cells naming the consumed aux, padding → input/output lengths + pad value, aux primitives → operands + result with the algebraic identity). Disclosures stay open across the byte-format toggle and across the debounced re-run after a param edit.
  • Round-key side-by-side panel — every round key of the active schedule renders as a labelled ribbon (AES → 11/13/15 4×4 grids; Serpent → 33 grids; Speck → 22 two-byte strips). The K_i whose canonical name appears in the current frame's auxRead map lights up, so chain XOR and Rcon column injection become visible at a glance.
  • Cell-level provenance hover — in the linear inspector, hover an output byte cell and the input cell(s) that feed it light up: same-position for SubBytes and AddRoundKey (whose round-key operand row lights up alongside the state), a gather for ShiftRows, the four same-column contributors for MixColumns (each annotated with its GF(2⁸) coefficient ×N). It's pure index math over the port-native primitives — derived from each step's params + port lengths, never the byte values, so it can't go stale — and operations whose byte mapping is only approximate (SHA-256's modular adds and bit-rotates) intentionally highlight nothing rather than mislead.
  • Compose-your-own block-cipher mode — ECB, CBC, CTR, CFB, and OFB ship built in for every cipher, but the primitives that build them are in the palette too: drag generic.aux-load, generic.aux-xor, and generic.aux-copy and wire up a mode by hand — your own feedback rule, or a rebuild of a shipped one to compare against it — around any single-block cipher, without writing a single line of executor code. Half-wired specs stay debuggable: missing aux references show up as orange ! glyphs on the canvas instead of throwing a runtime exception. (See src/steps/CLAUDE.md for the canonical CBC recipe.)
  • 2D graph view with drag-from-palette authoring — pan / drag containers, collapse groups, see the aux-flow + state-flow edges that connect every step. Drag step types from the left palette to insert them into the spec; warning glyphs surface orphaned reads, unused writes, cycles, and state-shape mismatches before you click Run. High-fanout sources (Serpent's 33-key schedule, AES's 11 round keys) collapse to local replica chips with the parallel arrows bundled into one ×N pill. Per-source edge colouring (Okabe-Ito 8-colour palette) makes overlapping dataflow readable at a glance. Replica chips and per-block iterate chips are draggable, with a per-node ↺ reset glyph and a toolbar [reset layout] button for the hard reset.
  • Rewire ports in place — change which upstream output feeds a step's input without touching code: click a leaf's input-port handle to arm it, then click any scope-legal source's bind handle to wire it (or use the per-port dropdown below the graph). Only same-scope sources are offered, so a wire can never crash at run time; a size-mismatch wire is allowed but flagged amber (the bytes coerce as a visible trace step). Rewires save and share like any other edit.
  • Save a group as a reusable element ("compose-and-save") — hover any group (e.g. an AES round) and click the chip in its header to capture it into a "my elements" palette section; drag it back anywhere to drop a fresh, fully editable copy. A composite is pure JSON (a saved group template, not opaque code), so its internals stay visible and scrubbable — the "cipher = JSON" idea at the reusable-block grain. The library persists per browser; the dropped copy inlines into the spec, so saved/shared .cipher.json files stay self-contained.
  • Duplicate a round — a + chip on any AES round container's header clones the round, renumbers subsequent rounds + their key indexes in lockstep, bumps the key schedule (Rcon table extended on the fly via xtime), and auto-mirrors onto the counterpart encrypt / decrypt spec so round-trip stays valid by construction. Answer "what would AES-128 with 11 rounds look like?" end-to-end.
  • Inline parameter editing on every step — swap the S-box, change the MixColumns matrix, edit a single byte of a round key, and watch the trace re-run in ~200 ms. The current step in the graph view stays focused across edits.
  • Per-frame state view (4×4 matrix or 1×N byte row) with before / after diff highlighting + FIPS-197 / paper references baked into each step's description panel.
  • Run history (last 5) with a diff overlay showing how the current run differs from any prior one, plus a Run Explorer modal for side-by-side comparison.
  • Byte format toggle: hex / decimal / ASCII, applied to every input + output cell. S-box axis labels stay hex (addresses, not values).
  • "Custom (was …)" indicator + one-click reset — the moment your spec diverges from the canonical cipher default (any palette insert, param edit, or delete), the header label and the cipher dropdown advertise the divergence; a reset button snaps you back.
  • Save / Load custom ciphers as .cipher.json documents (schemaVersion: 3, see src/core/document.ts). Layout pins (graph positions + collapsed groups) survive Save / Load. The "include session" toggle controls whether plaintext + key bytes are written.
  • Share via URL — a #doc=… hash carries the whole document (deflate-raw + base64url, ~2 KB even for AES-256 + session) so a paste-able link reproduces a custom cipher in a fresh tab. Spec-only shares are byte-stable (deterministic across builds) and public-safe (no plaintext, no key bytes).

Quickstart

git clone https://github.com/BoykoNeov/Cryptographer
cd Cryptographer
npm install
npm run dev

Open http://localhost:5173. Hot reload kicks in on every save.

Development

Command What it does
npm run dev Vite dev server with HMR.
npm test Vitest, single run. ~3515 tests across 270 files, ~90s (the jsdom UI tests dominate).
npm run typecheck tsc --noEmit, strict mode.
npm run check The full gate: biome ci . && tsc --noEmit && vitest run && vite build. ~100s warm; a cold first run can exceed 3 minutes.
npm run smoke Playwright real-browser smoke specs (graph drag/collapse, port wiring, composite save/drop, inspector cell-hover, …).
npm run build Production build into dist/. ~272 KB gzipped JS.

The pre-commit hook in .githooks/pre-commit runs npm run check. GitHub Actions runs the same on push.

Architecture (one paragraph)

The whole app is built around one idea: a cipher is JSON, not code. A CipherSpec (src/core/types.ts) is a tree of StepNodes. A Runtime (src/core/runtime.ts) walks the tree, looks each leaf's type up in the StepRegistry, and calls its pure executor (state, params) → state. Every executor registration includes a StepDocumentation block (name, summary, detail markdown, params, references) so the UI gets both behavior and docs from the same source. Adding a new cipher = registering its step types + authoring a JSON spec; no UI changes needed for new step types unless their params need a novel editor.

For the file-by-file map, read docs/key-files.md. For the cross-cutting footguns Claude has tripped on (AES pitfalls, Solid reactivity, PowerShell, padding overlay, multi-block iterate), see docs/gotchas.md.

Project layout

src/
  core/            cipher-agnostic engine: types, runtime, registry, spec mutations, document format, graph
  ciphers/         per-cipher specs + constants (AES, Speck, Serpent, DES, Blowfish,
                   Twofish, RSA, SHA-256, Keccak/SHA-3/SHAKE/cSHAKE/KMAC)
  ciphers/modes/   cipher-agnostic ECB / CBC / CTR / CFB / OFB over the BlockCipherCore contract
  steps/           step-type executors + their StepDocumentation blocks
  ui/              Solid components, stores, app shell
tests/             ~270 files, ~3515 tests — vitest (node + jsdom mix)
e2e/               Playwright real-browser smoke tests
docs/              key-files.md, gotchas.md, versioning.md, plans/
.githooks/         pre-commit gate

Versioning

Semver. App version lives in package.json and is re-exported from src/version.ts so the UI footer and any saved-document metadata.appVersion field stay in sync. The release log lives in CHANGELOG.md. Step-type contracts carry an @N suffix (e.g. aes.sub-bytes@1) and the document file format has its own schemaVersion — see docs/versioning.md for when each gets bumped and how migrations work.

License

Boyko Non-Commercial License v1.0 (BNCL-1.0) — free to use, modify, and distribute for non-commercial purposes. Commercial use requires a separate written permission from the copyright holder. See NOTICE for the short-form summary.

For Claude Code contributors

CLAUDE.md at the repo root carries the assistant brief: architecture summary, conventions (commit cadence, comment density, type safety), planning-mode rules, and pointers into docs/ and the user's auto-memory. Read it before non-trivial work.

About

Interactive cryptography explorer: see every step, every round, of ciphers like AES — and experiment with modifying them.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages