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.auand the older unkeyed one atqrng.anu.edu.au. Either or both can be enabled, and each has its owninterval_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
sizecounts. 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-sendor anything else that reads stdin.
Serving
- One 1024-bit value per request, in four forms:
/api/randomas JSON with the collection timestamp,/api/random/hexas 256 bare hex characters,/api/random/base64, and/api/random/binaryas exactly 128 raw bytes with no encoding and no trailing newline. All four carry the timestamp, source, size and remaining depth inX-Quantum-*response headers — the only place the raw form can put them — and setCache-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_entriesvalues, 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_fileand it is written under a passphrase taken fromWJ_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. Leavingpool_fileunset 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.
serveruns 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/helpis the same guidance as plain text for a terminal,GET /api/specan OpenAPI 3.1 description of the whole API,GET /api/statusthe pool depth, timestamp range, served total, last arrival and whether the pool is persistent.
Getting set up
setup sendandsetup recvask only what cannot be defaulted and write a working config, along with the companion files the diode tool needs.setup recvasks 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-configprints a config showing every option.statsasks 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
0600file rather than a command line.
Notes
- The API has no authentication and binds to
127.0.0.1:8099by 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_KEYloses the pool, and a diode carries nothing back to ask for the numbers again. served_historycosts 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.sizedefaults to 64 andanu.timeout_secondsto 180, measured against the live service, which leaves a comfortable margin.