moonseed0.2.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. 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. 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 extra-bits draw. A P-521 scalar is 66 bytes with the top bits clear, which is why this cannot be "ask for 32 bytes".

Both read-outs are pinned to fixed byte streams: the same bytes, read by an independent implementation of the same rule, give the same number, and every byte is accounted for.

// bound 1000: ten bits, so two bytes with six bits cleared test "the first draw is too big" { let source = @moonseed.Replay::new(b"\xff\xff\x03\xe7") // 0xffff becomes 0x03ff — 1023, still too big assert_eq(@moonseed.below(b"\x03\xe8", source), b"\x03\xe7") }

below(1 << k) never redraws — the mask already fits. Only a bound that is not a power of two pays for the loop. The block is a test, so moon test runs it.

To watch it happen, the bench draws the same bytes one attempt at a time.

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. The full note and an example beside each one are in the reference.

cobalt · marking
moonbitstack/moonseed3 functions · 1 trait · 1 type · 1 error
pub fn[S : Source] bytes(n : Int32, source : S) -> Bytes raise Unavailable
n uniform random bytes, no more and no fewer.
pub fn[S : Source] below(bound : BytesView, source : S) -> Bytes raise Unavailable
A uniform number in [0, bound), big-endian and as wide as bound.
pub fn[S : Source] scalar(order : BytesView, source : S) -> Bytes raise Unavailable
A uniform number in [1, order - 1], as wide as order: the private key or nonce of a group of that order.
pub(open) trait Source {
fn fill(Self, FixedArray[Byte]) -> Unit raise Unavailable
}
Where the bytes come from.
pub struct Replay
Replay::new(data : Bytes) -> Replay
A source that hands out the bytes it was given, in order, and is exhausted after.
Replay::fill(self : Replay, buf : FixedArray[Byte]) -> Unit raise Unavailable
Copy the next bytes of the replay into buf.
Replay::remaining(self : Replay) -> Int32
How many bytes are left to give.
pub(all) suberror Unavailable {
Platform
Exhausted
Failed(String)
} derive(Eq, Debug) · impl Show
No random bytes could be had.
moonbitstack/moonseed/system1 value · 1 type
pub let source : System
The one value there is a need for.
pub struct System
The platform's entropy source.
System::fill(self : System, buf : FixedArray[Byte]) -> Unit raise Unavailable
Fails with Platform when the platform has no source or reports a failure, and never falls back to anything 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.