moonseed0.1.0  ·  Apache-2.0
moonbitstack / moonseed

moonseed

Secure random bytes, drawn without bias.

You supply the source; it fills the buffer. No generator, no state, and no quiet fallback to a seeded one — when there are no bytes to be had, the call stops with Unavailable.

moon addmoonbitstack/moonseed

This tile was drawn from the bytes this page took, through below — by this library, compiled to wasm. Press it and the net breaks.

bytes taken— · browser
sourceReplay
samplingbelow · wasm
shards—
01

Source

One method, fills the buffer. Who fills it is the caller's business.

The crate is one contract: give me bytes. @system.source is the platform's — crypto.getRandomValues on JavaScript, WASI's random_get on wasm, the runtime on native.

Replay hands back the bytes it was given, in order, until it runs out — that is how the test vectors are replayed. Your own implementation can be a device, a service, a file. Three kinds of source, one method.

There is no fallback. That is not a promise in the prose, it is in the type: the call raises, the signature says so, and the caller cannot miss it.

celadon · surface
// one method, one error pub(open) trait Source { fn fill(Self, FixedArray[Byte]) -> Unit raise Unavailable } pub(all) suberror Unavailable { Platform // no entropy on this platform Exhausted // the replay is spent Failed(String) // your source failed, your reason }
02

Start

Three lines. No init, no global state, no generator to construct first.

// thirty-two uniform bytes; a negative length aborts let key = @moonseed.bytes(32, @system.source) // in [0, 1000), as wide as the bound let n = @moonseed.below(b"\x03\xe8", @system.source) // in [1, 4): a key, a nonce let d = @moonseed.scalar(b"\x05", @system.source)

moon run src/examples/tour walks the whole API once, so you can watch it ask for bytes.

mica · ground
@system.source
The platform. crypto on JS, WASI random_get on wasm, the runtime on native. It holds nothing; every fill asks again.
Replay::new(bytes)
The bytes given, in order, then Exhausted. Test vectors walk this path; remaining() says how many bytes are left.
impl Source for …
Your device, your service, your fixture. On failure raise Failed(reason) and the reason reaches the caller unchanged.
03

Sampling

Rejection, not modulo. Modulo favours the small values; not doing that is why this function exists.

below(bound) reads its bound as a big-endian number: draw exactly as many bytes as bound−1 needs, clear the unused high bits of the first, and draw again while the value is at or above bound. Every value is equally likely.

scalar(order) gives you something in [1, order) — a private key, a nonce. Draw as wide as order, draw again while the value is zero or not below order: FIPS 186-5 A.2.2, the same draw Go's ECDSA makes. A P-521 scalar is 66 bytes with the top bits clear, which is why this cannot be "ask for 32 bytes".

Both are checked against Go: fixed byte streams go into crypto/rand.Int and ecdsa.GenerateKey, and the tests replay those same bytes and expect the same values, every byte used.

// below(1000): 999 needs 10 bits, draw 2 bytes loop { source.fill(draw) // two bytes draw[0] = draw[0] & 0x03 // clear the top six let v = be(draw) if v < 1000 { return v } } // fewer than two draws on average

below(1 << k) never redraws — the mask already fits. Only a bound that is not a power of two pays for the loop.

bound 200 · twenty buckets cobalt · below copper-red · modulo expected · one hundred
Equal odds put a hundred in every bucket, two thousand over twenty. The modulo bars run thick at the low end and thin at the high, and the tilt is plain to see. moon-white · tally
04

API

Every public item, package by package. Nothing left out.

cobalt · marking
moonbitstack/moonseed5 functions · 1 trait · 1 type · 1 error
pub fn[S : Source] bytes(n : Int32, source : S) -> Bytes raise Unavailable
N uniform bytes, no more and no fewer. A negative n is a caller's mistake, so it aborts rather than pretend to hand back nothing.
pub fn[S : Source] below(bound : BytesView, source : S) -> Bytes raise Unavailable
[0, bound), as wide as the bound. Mask the unused high bits, redraw while at or above bound. A bound of 1 reads nothing; zero aborts.
pub fn[S : Source] scalar(order : BytesView, source : S) -> Bytes raise Unavailable
[1, order), as wide as the order. Keep the leftmost bits, redraw on zero or overflow: the FIPS 186-5 A.2.2 draw.
pub(open) trait Source {
fn fill(self, buf : FixedArray[Byte]) -> Unit raise Unavailable
}
The one thing you implement. Fill the buffer or raise; anything that keeps this contract plugs in without a line changed here.
pub struct Replay
Replay::new(data : Bytes) -> Self
A source that hands out what it was given, in order, then stops. Built from bytes alone, so test vectors replay through it.
Replay::fill(self, buf : FixedArray[Byte]) -> Unit raise Unavailable
When fewer bytes remain than the buffer wants, it gives every byte left and raises Exhausted. Not one byte is invented.
Replay::remaining(self) -> Int32
How many bytes are still to give. Ask this first when a vector does not match.
pub(all) suberror Unavailable {
Platform
Exhausted
Failed(String)
} derive(Eq, Debug) · impl Show
Platform: no source; Exhausted: the replay is spent; Failed: your source failed, and its reason reaches the caller unchanged. Eq, Show and Debug come with it.
moonbitstack/moonseed/system1 value · 2 types
pub let source : System
The platform's entropy — the one place this library touches the platform. crypto on JS, WASI random_get on wasm, the runtime on native.
pub struct System {}
An empty record that holds nothing: every fill asks the platform again, and nothing is cached between them.
System::fill(self, buf : FixedArray[Byte]) -> Unit raise Unavailable
One call, one answer. Short or refused, it raises Platform and turns nowhere else.
05

Limits

It is not a generator, and does not pretend to be one.

01

No generator

No seed, no stream, no "same seed gives the same numbers". For a reproducible stream, use moonrand. Keeping these apart is what stops either from deciding for you.

02

No WASI, no bytes

The root package touches no platform. Linking system links the platform's one call. A wasm host without WASI uses the root package and a source of its own.

03

No retry, no stretching

One call, one answer; whether to retry is the caller's policy. It moves entropy, it does not stretch it — 32 bytes do not become a 64-byte secret.

frosted · partition
It keeps no secret

It takes bytes from a source and hands them over. The buffer is yours to wipe or not; this crate does not allocate a cache or keep a copy.

No side-channel story

The rejection loop leaves a timing trace. Constant time is the caller's problem: bound the retries, or sample differently.

The platform is not verified

@system.source reports what the platform says. It does not judge whether the platform is compromised. That trust sits in the OS, not here.