Skip to content

Release Notes

wj-diode-quantum 1.0.0 - 25 August 2026

First release.

Collects quantum random numbers from the Australian National University on an internet-facing machine, pushes them across a one-way data diode, and serves them one at a time on the protected side. The two halves are one binary in two modes and never communicate: a diode carries data one way only, with no acknowledgement, so nothing here assumes the far side can be queried, told anything, or asked to resend.

Collecting

  • Draws from both ANU endpoints — the current keyed API at api.quantumnumbers.anu.edu.au and the older unkeyed one at qrng.anu.edu.au. Either or both can be enabled, and each has its own interval_seconds: eight hours for the keyed API, which allows roughly a hundred requests a month, and one hour for the legacy one, which allows one a minute.
  • Blocks returned by either API are concatenated and re-sliced into 1024-bit values locally, so the two endpoints' differing per-block limits do not matter — and neither does their disagreement over what size counts. Leftover bytes are discarded rather than padded, since padding would fabricate entropy ANU never sent. The number discarded per fetch is logged.
  • Each source is sent on its own, the moment it has answered, rather than a cycle collecting from everything due and sending one combined transfer. The two endpoints differ by two orders of magnitude in how long they take, so batching left the fast source's values waiting on the slow one and a failure anywhere discarded both.
  • Nothing is written to disk on this side. No outbox, no spool, no journal. Values exist only in memory and in the pipe to the diode. A transfer the diode will not take is retried within the cycle and then dropped, because there is nowhere to keep it and nobody to ask.
  • The wire format is JSON Lines, one value per line, written to the standard input of a configured send command — diodetool stream-send or anything else that reads stdin.

Serving

  • One 1024-bit value per request, in four forms: /api/random as JSON with the collection timestamp, /api/random/hex as 256 bare hex characters, /api/random/base64, and /api/random/binary as exactly 128 raw bytes with no encoding and no trailing newline. All four carry the timestamp, source, size and remaining depth in X-Quantum-* response headers — the only place the raw form can put them — and set Cache-Control: no-store.
  • A value is never served twice, including across a restart. The removal is recorded on disk before the value is written to the response, so a crash between the two can only lose the value, never hand it out again.
  • The pool is a bounded LIFO: newest served first, oldest discarded at capacity. It is a sliding window over the most recent max_entries values, which keeps served timestamps close to collection time and stops an old tail starving.
  • The pool can be kept across a restart, encrypted. Set server.pool_file and it is written under a passphrase taken from WJ_POOL_KEY, using AES-256-GCM with the key stretched by PBKDF2-HMAC-SHA256. The numbers are never written in the clear. Two files are used to make the ordering guarantee affordable: a small one holding the served history and the newest values, rewritten on every request, and a large one holding the rest, rewritten when values arrive and periodically. Leaving pool_file unset keeps the pool in memory only, and a restart then begins empty.
  • A pool file that will not decrypt stops the server rather than being discarded, so a wrong or missing key cannot destroy numbers the right one could still have read.
  • Validation is per line. One damaged line costs one value and parsing resumes at the next newline. A diode cannot retransmit, so containing the damage is all that can be done; rejecting the whole stream would throw away everything queued behind the fault.
  • serve runs and supervises the receiving tool itself, so the protected machine needs one process. A value split across two invocations of that tool is reassembled.
  • GET / is a self-contained documentation page with curl examples, an endpoint reference and live pool figures — no JavaScript, no external assets, readable in light or dark. GET /api/help is the same guidance as plain text for a terminal, GET /api/spec an OpenAPI 3.1 description of the whole API, GET /api/status the pool depth, timestamp range, served total, last arrival and whether the pool is persistent.

Getting set up

  • setup send and setup recv ask only what cannot be defaulted and write a working config, along with the companion files the diode tool needs. setup recv asks whether to keep the pool and generates a passphrase if so, printing it rather than writing it anywhere: a key stored beside the file it opens protects very little, and where it belongs is the operator's decision.
  • sample-config prints a config showing every option.
  • stats asks the running server over HTTP and prints pool depth, age and durability.
  • Configs are JSON and contain no paths on the sending side at all. Passwords go to a 0600 file rather than a command line.

Notes

  • The API has no authentication and binds to 127.0.0.1:8099 by default. Anything that can reach it can drain the pool, one value per request, permanently, and the collecting side can never be told stock is running low. Authentication and rate limiting belong in the layer in front of it.
  • The pool file protects the numbers where they travel without the key — backups, volume snapshots, a disk taken out of a machine — not against something that can already read both the file and the process environment. There is no key escrow and no recovery: losing WJ_POOL_KEY loses the pool, and a diode carries nothing back to ask for the numbers again.
  • served_history costs time as well as memory, since it is rewritten every time a value is served. At the defaults that is about 18 ms per request, so a persistent pool tops out near 55 requests a second — well above what either ANU endpoint can supply, but worth knowing if there is no rate limit in front. Lowering it raises the ceiling and shortens the replay-protection window.
  • The legacy ANU endpoint returns roughly two seconds per kilobyte and is being retired by ANU. anu.legacy.size defaults to 64 and anu.timeout_seconds to 180, measured against the live service, which leaves a comfortable margin.