Skip to content

Conventions

House rules for writing a page here. Readers browsing lessons do not need this file; it is for whoever is about to add one.

The rule that comes before the others

Every implementation in this repo is written to be read, and none of it is written to be used. A page that shows how AES-CBC works shows it in twenty lines of Python that would lose a key to anybody with a stopwatch. That is fine — that is the point — and it is only fine while the page says so.

So: any page whose example implements a primitive carries a "do not use this" line, in the prose, near the code, naming what to use instead. Not a disclaimer at the bottom; a sentence where the reader is. "Use your platform's AEAD — openssl enc -aes-256-gcm, libsodium's crypto_secretbox, Python's cryptography — and never this."

Two corollaries:

  • Never write an example that only makes sense as an attack. Every break demonstrated here is demonstrated against a target the page itself constructed, to show why the defence exists. A page about padding oracles builds the oracle; it does not point one at anything.
  • A page may not overstate what it proved. "This recovers the key in 3 seconds" is a claim about this toy; say so in the same sentence.

The shape of a lesson

04_Hashing/
  a_hash_is_not_encryption/
    README.md                                the lesson
    examples/
      a_hash_is_not_encryption_py.py         the Python program
      a_hash_is_not_encryption_py.out        its recorded output
      a_hash_is_not_encryption_sh.sh         the shell script
      a_hash_is_not_encryption_sh.out
      a_hash_is_not_encryption_rs.rs         (optional) the Rust view

One idea per folder. The folder name is the idea, in lower_snake_case, and it becomes a permanent URL — so name it for what it teaches, not for where it currently sits in the reading order.

A lesson does not need all three languages. It needs the ones that show something the others cannot: Python for the shortest statement and the arithmetic, the shell for the real primitive and the bytes on a real pipe, Rust for the type or the byte-level view. A lesson with one example is fine; a lesson with three that say the same thing is padding.

The page

Open with the title, then two lines that let a reader decide in five seconds whether this is their page:

# A hash is not encryption

**Level:** 101 · for anyone starting from zero

**One line:** A hash has no key and no inverse. Nothing "decrypts" a SHA-256 — the input is not in there.

**Level:** is 101 / 201 / 301 / reference, then ·, then who it is for. The one-line summary states the claim, not the topic.

Then, in this order: the mechanism in prose, the generated blocks per language (## In Python, ## In the terminal, ## In Rust), the bridge, ## Try it, ## See also.

Do not hard-wrap paragraphs. Write each paragraph as one long line and let the editor soft-wrap; Markdown collapses single newlines anyway.

Output is generated, never typed

Mark the spot and let the tool fill it:

<!-- output:a_hash_is_not_encryption_py -->
<!-- /output -->

tools/run_examples.py runs the program and pastes what it actually printed, with a provenance line above the fence. Inside the markers is generated; outside is yours. The stem is bare — no path, no extension — so stems must be unique repo-wide across languages, which is what the _py / _rs / _sh suffix is for.

There is a second kind, <!-- source:stem -->, which pastes the program itself. Use it when the code is the lesson.

python3 tools/run_examples.py                      # verify + refill
python3 tools/run_examples.py --update --only X    # record X's output as its answer key
python3 tools/run_examples.py --check              # write nothing, fail on drift (CI)
python3 tools/check_all.py                         # every gate CI runs, honest exit status

Always pass --only with --update, and read what it recorded before committing: --update accepts whatever the program printed, so it will happily enshrine a bug.

The programs

Python: stdlib only. hashlib, hmac, secrets, os.urandom, int.from_bytes/to_bytes, and pow(base, exp, mod) — which is a complete modular-exponentiation engine and is all Diffie–Hellman and textbook RSA need. No cryptography, no pycryptodome. Where the stdlib genuinely stops (AES, X25519, Ed25519), the shell example calls openssl and the page says which door it went through.

Rust: bare rustc --edition 2024. No Cargo, no crates, so no RustCrypto either. Rust's job here is the byte-level and type-level view — fixed-size arrays, u8 arithmetic, what a "constant-time" comparison has to avoid.

Shell: bash, plus openssl and the tools both macOS and Ubuntu shipxxd, printf, head, wc, base64, shasum.

Deterministic. No clocks, no network, and — the one that bites in this library — no randomness in an answer key. An example that generates a key must either use a fixed test vector or print a property of the value rather than the value: len(k) == 32, k != k2, int.from_bytes(k) % 2 is not recordable but "the two differ" is. Any example that prints secrets.token_hex() cannot have a key at all; print what you can check instead.

Use published test vectors where they exist, and name them. SHA-256("abc"), RFC 4231's HMAC vectors, NIST's AES-CBC vectors: these are values the whole world has agreed on, so an answer key holding one is checking your program, not recording an accident.

Written to be read aloud. Numbered sections, aligned columns, prose in the print statements.

A snippet in the prose puts its output in a trailing comment, on the line that prints it, so the whole thing survives a copy-paste:

hashlib.sha256(b"abc").hexdigest()[:16]   # 'ba7816bf8f01cfea'

Never open a page with code that does not run. The first block on a page is the one that gets pasted.

Two openssls

/usr/bin/openssl is LibreSSL on macOS and OpenSSL 3 on Linux, and CI runs both. They agree on every ciphertext and every digest, because those are standards; they disagree about presentation and about which subcommands exist. Measured 2026-09-07 — LibreSSL 3.3.6 (/usr/bin/openssl, macOS 26.6) against OpenSSL 3.6.4:

LibreSSL OpenSSL 3
openssl dgst -sha256 on abc ba7816bf…20015ad SHA2-256(stdin)= ba7816bf…20015ad
openssl dgst -sha256 -r ba7816bf…20015ad *stdin identical
openssl dgst -sha256 -binary \| xxd -p ba7816bf… identical
openssl enc -aes-256-cbc -K … -iv … b4278704fb5d45e049f645bc96155802 identical
openssl kdf 'kdf' is an invalid command a subcommand

The digest is the same digest; only the label differs — which is the nastiest shape this takes, because a red CI line then reads as though the crypto disagreed. So:

  • Never record a bare openssl dgst line. Record -r, or -binary | xxd -p.
  • Check a subcommand exists on both before an example depends on it. kdf, pkeyutl -kdf, and most of the newer -provider machinery are OpenSSL-3-only.
  • A third openssl may be first on your PATH. This was written on a Mac where Homebrew's OpenSSL 3.6.4 shadows Apple's LibreSSL at /usr/local/bin/openssl, so a local run silently agreed with Ubuntu and would have failed on the macOS runner. If an example's output depends on the implementation, say /usr/bin/openssl explicitly — or do not record it.

More of these will turn up. Add a row when one does; the list is the point.

Bridges

Every lesson has a section If you are coming from Python or ABAP — the two languages this library's reader already thinks in.

The ABAP half is prose. CI cannot run ABAP, so every page says so: (Not machine-checked — CI cannot run ABAP.) Keep ABAP claims to things you would bet on — cl_abap_hmac, cl_abap_message_digest, cl_sec_sxml_writer, the secure store — and never quote an SAP note number or a profile parameter from memory; name it as something to verify on the system.

  • Link a folder by naming its README.md[label](some_folder/README.md), never [label](some_folder/).
  • A repo path in backticks should be a link, not bare code text: backticks in the label, a real relative path in the href.
  • A link that leaves the library ends its label with ; an internal link never does. python3 tools/check_link_style.py --fix adds and removes them; CI runs it without --fix.
  • Where a sibling library already teaches something — ASCII layout, UTF-8, u8, hex — link to it and do not repeat it.

Stubs

A stub is a lesson page with no example behind it yet: an H1, a **Level:**, the notice, a **One line:**, and the questions the finished page has to answer. Every stub carries this notice directly under its **Level:** line:

> **Stub — an outline, not a lesson.** There is no runnable example behind this page yet, so nothing on it has been through [the check that backs every other claim in this library](../../CONTRIBUTING.md). The bullets below are the questions the finished page has to answer.

A stub must not have an <!-- output: --> block — there is nothing to fill it from. It graduates by gaining an examples/ program and losing the notice; update its row in the chapter README and in ROADMAP.md when it does.