feat(stdlib): finish Crypto (CSPRNG + base64) and add Uuid.* library (#19 #16)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Crypto (#19): add the OS cryptographically-secure random surface (random_bytes/random_hex/random_u32, reading /dev/urandom) and a standard base64 encoder, completing the library alongside the existing SHA-256/ HMAC-SHA256/verify_hmac/hex/ct_equal. All pure integer IR, C-free. Uuid (#16): a new namespace for stable, collision-free IDs — v4 (random) and v7 (time-ordered) generation, plus parse/is_valid/to_text/equals/nil. UUIDs are canonical lowercase 36-char strings; v4 and v7's random tail draw from the crypto CSPRNG, so both carry the documented determinism caveat (mint at the edges, never inside lockstep simulation). Reuses the crypto prelude's fn_secure_bytes / fn_hex_encode. - examples/library/{crypto,uuid}.ludic: known-answer vectors (SHA-256, HMAC, base64 per RFC 4231/4648) and structural invariants (uuid version/variant bits, parse/equals), wired into `x test` (now 51 passed). - docs: per-symbol pages for every new method + a new Uuid section; inventory and impl-vs-docs coverage check pass. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a422ef4375
commit
2ddf830f0b
26 changed files with 12185 additions and 10720 deletions
|
|
@ -8,4 +8,6 @@ Secure, test-vector-backed hashing for the handful of security-sensitive things
|
|||
|
||||
Digests are returned as lowercase hex strings, not raw bytes — a <code>str</code> is null-terminated and a raw digest can contain a zero byte, so hex is the form you can print, store, and compare directly.
|
||||
|
||||
Alongside hashing, this library exposes the OS cryptographically-secure random generator — <a href="crypto-random_bytes"><code>random_bytes</code></a>, <a href="crypto-random_hex"><code>random_hex</code></a>, and <a href="crypto-random_u32"><code>random_u32</code></a> — for tokens, nonces, and <a href="ns-Uuid"><code>Uuid</code></a> generation, plus <a href="crypto-base64"><code>base64</code></a> for moving bytes through text-only channels. The secure-random helpers are deliberately non-deterministic and must never seed the lockstep simulation RNG (<a href="ns-Random"><code>Random</code></a>).
|
||||
|
||||
What this is not: it is not DRM and it is not unbeatable anti-cheat. A client-side game cannot keep a secret from the machine it runs on — a determined owner can always read the key out of the binary. Use it to make *casual* tampering detectable and to authenticate messages between parties who share a key. To verify a MAC always use <a href="crypto-verify_hmac"><code>Crypto.verify_hmac</code></a> (a constant-time check), never <code>==</code>, which leaks how much of a guessed MAC was correct.
|
||||
|
|
|
|||
28
docs/language/crypto/crypto-base64.md
Normal file
28
docs/language/crypto/crypto-base64.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: crypto-base64
|
||||
name: Crypto.base64
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.base64
|
||||
sig: Crypto.base64(s) -> string
|
||||
tip: Standard base64 (RFC 4648) of a string's bytes.
|
||||
order: 9
|
||||
ns: Crypto
|
||||
member: base64
|
||||
---
|
||||
|
||||
Encodes the bytes of <code>s</code> as standard base64 (RFC 4648, the <code>A–Z a–z 0–9 + /</code> alphabet with <code>=</code> padding). Base64 turns arbitrary bytes into printable ASCII, which is what you want when a digest, key, or binary blob has to travel through a text-only channel — a JSON field, a URL-safe token wrapper, a config file, a log line. It is an *encoding*, not encryption: it hides nothing and adds no integrity. Pair it with <a href="crypto-hmac_sha256"><code>Crypto.hmac_sha256</code></a> when the payload must also be tamper-evident.
|
||||
|
||||
The output length is always a multiple of four; a one- or two-byte remainder in the input is padded with <code>=</code>.
|
||||
|
||||
Parameters:
|
||||
- `s` — the string whose bytes are encoded
|
||||
|
||||
```ludic
|
||||
program Encode {
|
||||
entry {
|
||||
print(Crypto.base64("foobar")) # Zm9vYmFy
|
||||
print(Crypto.base64("f")) # Zg==
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/crypto/crypto-random_bytes.md
Normal file
28
docs/language/crypto/crypto-random_bytes.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: crypto-random_bytes
|
||||
name: Crypto.random_bytes
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.random_bytes
|
||||
sig: Crypto.random_bytes(n) -> string
|
||||
tip: n bytes from the OS CSPRNG, as a 2n-character hex string.
|
||||
order: 6
|
||||
ns: Crypto
|
||||
member: random_bytes
|
||||
---
|
||||
|
||||
Draws <code>n</code> bytes from the operating system's cryptographically-secure random number generator and returns them as a <code>2n</code>-character lowercase hex string. Use it for unguessable tokens, nonces, and session secrets — anything whose whole value is that an attacker cannot predict it. The result is hex rather than raw bytes for the same reason digests are: a <code>str</code> is null-terminated and raw random bytes can contain a zero byte, so hex is the form you can safely store and compare.
|
||||
|
||||
This is deliberately **non-deterministic** — it must never seed the lockstep simulation RNG (<a href="ns-Random"><code>Random</code></a>). Two calls return different values. On a platform without an OS CSPRNG (for example a bare wasm target) the draw degrades to zeroes rather than faulting; treat a real CSPRNG there as a platform-layer responsibility.
|
||||
|
||||
Parameters:
|
||||
- `n` — the number of secure random bytes to draw
|
||||
|
||||
```ludic
|
||||
program Token {
|
||||
entry {
|
||||
let session = Crypto.random_bytes(16) # 32 hex chars, unguessable
|
||||
print(len(session)) # 32
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/crypto/crypto-random_hex.md
Normal file
26
docs/language/crypto/crypto-random_hex.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: crypto-random_hex
|
||||
name: Crypto.random_hex
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.random_hex
|
||||
sig: Crypto.random_hex(n) -> string
|
||||
tip: Alias for random_bytes — n secure bytes as a 2n-char hex string.
|
||||
order: 7
|
||||
ns: Crypto
|
||||
member: random_hex
|
||||
---
|
||||
|
||||
Identical to <a href="crypto-random_bytes"><code>Crypto.random_bytes</code></a>: draws <code>n</code> bytes from the OS CSPRNG and returns them as a <code>2n</code>-character lowercase hex string. The two names are interchangeable; <code>random_hex</code> exists so call sites can be explicit that the return value is already hex text (not raw bytes) when that reads more clearly. The same determinism caveat applies — the result is unpredictable by design and must stay out of the reproducible simulation RNG.
|
||||
|
||||
Parameters:
|
||||
- `n` — the number of secure random bytes to draw
|
||||
|
||||
```ludic
|
||||
program Nonce {
|
||||
entry {
|
||||
let nonce = Crypto.random_hex(12) # 24 hex chars
|
||||
print(len(nonce)) # 24
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/crypto/crypto-random_u32.md
Normal file
26
docs/language/crypto/crypto-random_u32.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: crypto-random_u32
|
||||
name: Crypto.random_u32
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.random_u32
|
||||
sig: Crypto.random_u32() -> int
|
||||
tip: One CSPRNG-drawn 32-bit integer.
|
||||
order: 8
|
||||
ns: Crypto
|
||||
member: random_u32
|
||||
---
|
||||
|
||||
Draws four bytes from the OS CSPRNG and assembles them into one 32-bit integer. Use it when you need a single unpredictable number rather than a hex string — a random challenge value, a per-run identifier, or a non-deterministic seed to hand to a *fresh* <a href="ns-Random"><code>Random</code></a> stream at startup. Because the bytes come from the secure generator, the value prints as a signed integer and can be negative.
|
||||
|
||||
Like the other secure-random helpers this is **non-deterministic** and must not be called inside the lockstep simulation: doing so desyncs replays and networked peers. Draw it at the edges (startup, on connect) and, if you need reproducible gameplay randomness afterward, seed <a href="random-seed"><code>Random.seed</code></a> with it once.
|
||||
|
||||
```ludic
|
||||
program Challenge {
|
||||
entry {
|
||||
let c = Crypto.random_u32()
|
||||
Random.seed(value: c) # non-deterministic seed, chosen once at startup
|
||||
print(Random.int(100))
|
||||
}
|
||||
}
|
||||
```
|
||||
13
docs/language/uuid/_section.md
Normal file
13
docs/language/uuid/_section.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
id: uuid
|
||||
title: Uuid
|
||||
order: 6
|
||||
---
|
||||
|
||||
Universally-unique identifiers — stable IDs that don't collide, generated locally with no central authority handing out numbers. Reach for a UUID whenever something needs an identity that survives being saved, shared, or sent over a network: players and sessions in multiplayer, user-created content (levels, items, mods) that has to merge cleanly across installs, or a per-install / per-run ID for analytics and bug reports.
|
||||
|
||||
Two versions are provided. <a href="uuid-new"><code>Uuid.new</code></a> makes a **v4** (random) UUID — 122 bits of entropy, effectively never colliding. <a href="uuid-new_v7"><code>Uuid.new_v7</code></a> makes a **v7** (time-ordered) UUID whose leading bits are a millisecond timestamp, so a batch of v7 IDs sorts by creation time — friendly to database indexes and append logs. Both set the RFC 4122 version and variant bits correctly.
|
||||
|
||||
A UUID is represented as its canonical lowercase 36-character text form (<code>8-4-4-4-12</code>), the same shape you store, print, send, and compare — so there is no conversion at each boundary. Validate untrusted input with <a href="uuid-is_valid"><code>Uuid.is_valid</code></a> or normalise it with <a href="uuid-parse"><code>Uuid.parse</code></a>, and compare with <a href="uuid-equals"><code>Uuid.equals</code></a>, which ignores case.
|
||||
|
||||
**Determinism caveat.** v4 and the random tail of v7 come from the OS cryptographically-secure RNG (<a href="ns-Crypto"><code>Crypto</code></a>), which is non-deterministic by design. Minting a UUID inside the lockstep simulation will desync replays and networked peers — generate IDs at the edges (on connect, on save, on spawn-from-input), never per tick in reproducible gameplay code.
|
||||
27
docs/language/uuid/uuid-equals.md
Normal file
27
docs/language/uuid/uuid-equals.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: uuid-equals
|
||||
name: Uuid.equals
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.equals
|
||||
sig: Uuid.equals(a, b) -> bool
|
||||
tip: Case-insensitive UUID equality.
|
||||
order: 8
|
||||
ns: Uuid
|
||||
member: equals
|
||||
---
|
||||
|
||||
Compares two UUIDs for equality, ignoring case. UUIDs generated by this library are always lowercase, so a plain <code>==</code> works between them — but a UUID that arrived from another system may be upper- or mixed-case, and <code>equals</code> matches it correctly without you having to normalise first. Values of different length are never equal.
|
||||
|
||||
Parameters:
|
||||
- `a`, `b` — the UUID strings to compare
|
||||
|
||||
```ludic
|
||||
program Eq {
|
||||
entry {
|
||||
let lo = "550e8400-e29b-41d4-a716-446655440000"
|
||||
let up = "550E8400-E29B-41D4-A716-446655440000"
|
||||
if Uuid.equals(lo, up) { print(1) } # case-insensitive
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/uuid/uuid-is_valid.md
Normal file
28
docs/language/uuid/uuid-is_valid.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: uuid-is_valid
|
||||
name: Uuid.is_valid
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.is_valid
|
||||
sig: Uuid.is_valid(s) -> bool
|
||||
tip: Is s a well-formed UUID string?
|
||||
order: 6
|
||||
ns: Uuid
|
||||
member: is_valid
|
||||
---
|
||||
|
||||
Reports whether <code>s</code> is a well-formed UUID: exactly 36 characters, hyphens at positions 8, 13, 18 and 23, and hexadecimal digits (either case) everywhere else. This is the guard to run on any UUID that came from outside your program — a save file, a network message, a mod, a command-line flag — before you trust it as an identity.
|
||||
|
||||
It checks *shape*, not version: both v4 and v7 IDs (and any other conforming UUID) pass. To also fold a malformed value into a safe default in one step, use <a href="uuid-parse"><code>Uuid.parse</code></a> instead.
|
||||
|
||||
Parameters:
|
||||
- `s` — the string to check
|
||||
|
||||
```ludic
|
||||
program Valid {
|
||||
entry {
|
||||
if Uuid.is_valid("550e8400-e29b-41d4-a716-446655440000") { print(1) }
|
||||
if not Uuid.is_valid("not-a-uuid") { print(2) }
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/uuid/uuid-new.md
Normal file
25
docs/language/uuid/uuid-new.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: uuid-new
|
||||
name: Uuid.new
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.new
|
||||
sig: Uuid.new() -> string
|
||||
tip: A new random (v4) UUID as a canonical 36-char string.
|
||||
order: 1
|
||||
ns: Uuid
|
||||
member: new
|
||||
---
|
||||
|
||||
Generates a new **v4** (random) UUID and returns it in canonical lowercase text form, e.g. <code>550e8400-e29b-41d4-a716-446655440000</code>. A v4 UUID carries 122 bits of entropy drawn from the OS secure random generator, so two independently-generated IDs colliding is not something you will ever observe in practice — which is exactly what makes it a good identity for a player, a session, a networked entity, or a piece of user-created content that has no central authority to number it.
|
||||
|
||||
This is the default UUID constructor; <a href="uuid-v4"><code>Uuid.v4</code></a> is an explicit alias. Because the value is random it is **non-deterministic** — do not mint UUIDs inside the lockstep simulation, or replays and peers will diverge.
|
||||
|
||||
```ludic
|
||||
program NewId {
|
||||
entry {
|
||||
let id = Uuid.new()
|
||||
print(len(id)) # 36
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/uuid/uuid-new_v7.md
Normal file
25
docs/language/uuid/uuid-new_v7.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: uuid-new_v7
|
||||
name: Uuid.new_v7
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.new_v7
|
||||
sig: Uuid.new_v7() -> string
|
||||
tip: A new time-ordered (v7) UUID; sorts by creation time.
|
||||
order: 3
|
||||
ns: Uuid
|
||||
member: new_v7
|
||||
---
|
||||
|
||||
Generates a new **v7** (time-ordered) UUID. Its leading 48 bits are a Unix-millisecond timestamp, so a batch of v7 IDs sorts lexicographically by creation time — which keeps database indexes and append-only logs tidy in a way random v4 IDs do not. The remaining bits are secure random, so IDs minted in the same millisecond are still distinct, and the RFC 4122 version (7) and variant bits are set.
|
||||
|
||||
Sub-second resolution is derived from the wall clock in seconds (multiplied to milliseconds), so ordering is guaranteed at one-second granularity with the random tail breaking ties within a second. Like v4 this reads the non-deterministic wall clock and CSPRNG — generate at the edges, never inside reproducible simulation. <a href="uuid-v7"><code>Uuid.v7</code></a> is an alias.
|
||||
|
||||
```ludic
|
||||
program NewV7 {
|
||||
entry {
|
||||
let id = Uuid.new_v7()
|
||||
print(id[14..15]) # 7 — the version nibble
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/uuid/uuid-nil.md
Normal file
23
docs/language/uuid/uuid-nil.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: uuid-nil
|
||||
name: Uuid.nil
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.nil
|
||||
sig: Uuid.nil() -> string
|
||||
tip: The all-zero UUID.
|
||||
order: 9
|
||||
ns: Uuid
|
||||
member: nil
|
||||
---
|
||||
|
||||
Returns the nil UUID — <code>00000000-0000-0000-0000-000000000000</code> — the reserved all-zero value that means "no UUID". Use it as a sentinel for an unset or absent identity, and as the value <a href="uuid-parse"><code>Uuid.parse</code></a> returns when it is handed something that is not a valid UUID. It is a well-formed UUID string, so it passes <a href="uuid-is_valid"><code>Uuid.is_valid</code></a>; test for "no id" by comparing against <code>Uuid.nil()</code> explicitly.
|
||||
|
||||
```ludic
|
||||
program Nil {
|
||||
entry {
|
||||
let none = Uuid.nil()
|
||||
print(none) # 00000000-0000-0000-0000-000000000000
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/uuid/uuid-parse.md
Normal file
29
docs/language/uuid/uuid-parse.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
id: uuid-parse
|
||||
name: Uuid.parse
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.parse
|
||||
sig: Uuid.parse(s) -> string
|
||||
tip: Normalise an untrusted string to a lowercase UUID, or the nil UUID.
|
||||
order: 5
|
||||
ns: Uuid
|
||||
member: parse
|
||||
---
|
||||
|
||||
Normalises an untrusted string: if <code>s</code> is a well-formed UUID it returns the same value in canonical lowercase form; if it is not, it returns the <a href="uuid-nil"><code>nil</code></a> UUID. This never faults on garbage, so it is safe to call on data that arrived from a file, a network peer, or a mod. When you need to *reject* bad input rather than fold it to nil, gate on <a href="uuid-is_valid"><code>Uuid.is_valid</code></a> first.
|
||||
|
||||
Parsing accepts either case and yields the lowercase canonical form, which is what the rest of the library produces — so a parsed ID compares equal (with <code>==</code>) to a freshly generated one of the same value.
|
||||
|
||||
Parameters:
|
||||
- `s` — the string to validate and normalise
|
||||
|
||||
```ludic
|
||||
program Parse {
|
||||
entry {
|
||||
let id = Uuid.parse("550E8400-E29B-41D4-A716-446655440000")
|
||||
print(id[0..8]) # 550e8400 — lowercased
|
||||
if Uuid.parse("nope") == Uuid.nil() { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/uuid/uuid-to_text.md
Normal file
26
docs/language/uuid/uuid-to_text.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: uuid-to_text
|
||||
name: Uuid.to_text
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.to_text
|
||||
sig: Uuid.to_text(id) -> string
|
||||
tip: The canonical text form of a UUID.
|
||||
order: 7
|
||||
ns: Uuid
|
||||
member: to_text
|
||||
---
|
||||
|
||||
Returns the canonical 36-character text form of <code>id</code>. Because this library already represents every UUID as its canonical lowercase text, <code>to_text</code> is the identity function — it exists so intent is explicit at the point where a UUID is turned into a string for display, storage, or a wire payload, and so code stays correct if the internal representation ever changes to a packed 128-bit value.
|
||||
|
||||
Parameters:
|
||||
- `id` — the UUID to render
|
||||
|
||||
```ludic
|
||||
program ToText {
|
||||
entry {
|
||||
let id = Uuid.new()
|
||||
print(len(Uuid.to_text(id))) # 36
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/uuid/uuid-v4.md
Normal file
24
docs/language/uuid/uuid-v4.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: uuid-v4
|
||||
name: Uuid.v4
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.v4
|
||||
sig: Uuid.v4() -> string
|
||||
tip: Explicit alias for Uuid.new — a random (v4) UUID.
|
||||
order: 2
|
||||
ns: Uuid
|
||||
member: v4
|
||||
---
|
||||
|
||||
An explicit alias for <a href="uuid-new"><code>Uuid.new</code></a>: generates a **v4** (random) UUID in canonical lowercase text form. Use this spelling when the surrounding code also uses <a href="uuid-v7"><code>Uuid.v7</code></a> and naming both by version reads more clearly than <code>new</code> versus <code>new_v7</code>. The behaviour, entropy, and non-determinism caveat are identical to <code>Uuid.new</code>.
|
||||
|
||||
```ludic
|
||||
program V4 {
|
||||
entry {
|
||||
let a = Uuid.v4()
|
||||
let b = Uuid.v4()
|
||||
if a != b { print(1) } # two draws differ
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/uuid/uuid-v7.md
Normal file
23
docs/language/uuid/uuid-v7.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: uuid-v7
|
||||
name: Uuid.v7
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.v7
|
||||
sig: Uuid.v7() -> string
|
||||
tip: Explicit alias for Uuid.new_v7 — a time-ordered UUID.
|
||||
order: 4
|
||||
ns: Uuid
|
||||
member: v7
|
||||
---
|
||||
|
||||
An explicit alias for <a href="uuid-new_v7"><code>Uuid.new_v7</code></a>: generates a **v7** (time-ordered) UUID whose leading bits are a millisecond timestamp so the IDs sort by creation time. Use this spelling to sit symmetrically beside <a href="uuid-v4"><code>Uuid.v4</code></a>. Behaviour, timestamp resolution, and the determinism caveat are identical to <code>Uuid.new_v7</code>.
|
||||
|
||||
```ludic
|
||||
program V7 {
|
||||
entry {
|
||||
let id = Uuid.v7()
|
||||
if Uuid.is_valid(id) { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue