Random Numbers & Crypto​

Randomness is a value you own. A random object holds one generator: you make it, draw from it, copy it, save it and seed it, and nothing else in the program can move it. There is no hidden global stream to reseed by accident, as there is with C's rand. A seeded generator draws the same numbers on every platform, compiler and backend, which is what replays, tests and rollback netcode need. Secrets (tokens, keys, salts, passwords) come from crypto:: instead, which reads the operating system's secure generator and can never be seeded.

A generator you own​

Declare one with random r; and draw from it. Without a seed it starts from 32 bytes of the operating system's secure generator, so every run differs. next_int(lo, hi) includes both bounds, next_float() is in [0, 1), next_float(lo, hi) in [lo, hi), and next_bool() is a fair coin. pick(values) returns one element, shuffle(values) shuffles an array in place, and next_bytes(n) returns a string of n bytes.

jaiscript
random dice;                          // seeded by the operating system: new numbers every run
int roll = dice.next_int(1, 6);
print(roll >= 1 && roll <= 6);

random r(2024);                       // seeded: the same numbers every run, on every machine
print(r.next_int(1, 6), " ", r.next_int(1, 6), " ", r.next_float(), " ", r.next_bool());
print(r.next_float(-1.0, 1.0));
array<string> deck = ["ace", "king", "queen", "jack"];
r.shuffle(deck);
print(deck.join(" "), " / ", r.pick(deck), " / ", r.next_bytes(4).size(), " bytes");
output
true
1 5 0.07205494006296331 false
0.5473007794378619
queen jack king ace / king / 4 bytes

An inverted range raises random::next_int range is inverted (max < min) (or random::next_float range is inverted (max < min)); next_float(lo, hi) with an infinite or NaN bound raises random::next_float bounds must be finite; pick of an empty array raises random::pick expects a non-empty array, shuffle of anything but an array random::shuffle expects an array, and a negative count random::next_bytes count cannot be negative. Under strict checking the constructor and every method are held to their signatures: random r("x");, random r(1, 2); and r.next_int(1) fail the load.

Seeds, replays and rollback​

A seed fixes the stream. random r(seed);, random r = random(seed); and r.seed(seed) all start the same stream. A float seed converts as every int parameter of a bound function does: toward zero, saturating at the int range, NaN as 0. A random is a value like int: a copy holds the same state and replays the same draws, in a variable, a parameter, a field, an array or a map. (An array or map literal shares a C++-backed object it names, as it does every bound object, so [r, r] holds r twice; build an array of generators from calls, [random(1), random(2)], or with push, which copies.) So a snapshot is an assignment and a rollback is another.

jaiscript
random a(7);
random b = random(7);
random c(1);
c.seed(7);                            // restarts c as random(7) would
print(a.next_int(0, 99), " ", b.next_int(0, 99), " ", c.next_int(0, 99));

random world(99);
world.next_int(0, 9);
random saved = world;                 // a copy holds the same state: a snapshot
int before = world.next_int(0, 1000);
world = saved;                        // roll back
print(before == world.next_int(0, 1000));

string json = to_json(saved);
print(json);
var restored = from_json(json);
print(type_of(restored), " ", restored.next_int(0, 1000) == before, " ", from_base64(to_base64(saved)).next_int(0, 1000) == before);
print(saved.state.size(), " ", from_json(json).state == saved.state);
output
70 70 70
true
{"$type":"random","$val":{"state":"50d88ab272bd2e909f5bc01a3b9bd0bce57076386f4eb91848ae6245647087df"}}
random true true
64 true

r.state is the generator's whole state as 64 lowercase hex digits; storing one into r.state resumes that stream exactly, and anything but 64 hex digits (or all zeros) raises random::state expects 64 hex digits, not all zero. to_json, from_json, to_base64 and from_base64 carry it, so a save file or a rollback snapshot restores the generator mid-stream. A host holds the same generator as jai::stdlib::random (<jaiscript/stdlib/random.hpp>), and its save and load write the same state through the C++ archives.

The algorithm, exactly​

Every formula below is part of the language, so a seed draws the same bits everywhere:

StepDefinition
generatorxoshiro256** 1.0 (Blackman and Vigna): four 64-bit words; a draw is rotl(s1 * 5, 7) * 9
seedingthe words are four successive splitmix64 outputs from the seed: x += 0x9E3779B97F4A7C15, then z = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9, z = (z ^ (z >> 27)) * 0x94D049BB133111EB, z ^ (z >> 31)
next_int(lo, hi)Lemire's method over span = hi − lo + 1 (the high word of draw × span, redrawn while the low word falls under (264 − span) mod span); the whole int range is one raw draw
next_float()(draw >> 11) × 2−53
next_float(lo, hi)fma(u, hi − lo, lo) with u = next_float(), one rounding; a result that rounds up to hi is the float just below it
next_bool()the draw's top bit
next_bytes(n)one draw per 8 bytes, lowest byte first
shuffle, pickFisher–Yates from the last index down, element i swapped with the one at next_int(0, i); pick takes the element at next_int(0, size − 1)

In parallel loops​

A parallel body can draw from a random it captures, in the body or in the functions, methods and constructors it calls. The statement takes one draw from the captured generator before any element runs; element i (the chunk, for parallel_reduce) then draws from its own stream, the one splitmix64 seeds from mix(draw ^ mix(i + 0x9E3779B97F4A7C15)) (mix being the output step above). So the loop gives the same numbers on any number of cores, and afterwards the captured generator has moved exactly one draw for each statement that captured it. The numbers are not the serial loop's: a serial loop draws in element order and a parallel one has no order. A random an element owns (an array<random> zipped beside the data, or a field of the element's class) or one the body makes draws its own stream as usual, so a field drawn in a parallel loop gives the serial loop's numbers. Each element must own its generator: two elements holding one object are refused before any element runs, with parallel_for: elements 1 and 2 share one 'random' in field 'brain' (an element owns the C++ values it holds: give each element its own, or capture one from outside the loop).

jaiscript
random loot(5);
array<int> drops = [0, 0, 0, 0, 0, 0, 0, 0];
parallel_for (auto& d : drops) { d = loot.next_int(1, 100); }   // the same drops on any number of cores
print(drops.join(" "));
print(loot.next_int(1, 100));                                     // the generator moved one draw for the loop

array<random> brains = [random(1), random(2), random(3)];        // one stream per monster
array<int> hp = [10, 10, 10];
parallel_for (auto& h, auto& brain : hp, brains) { h -= brain.next_int(1, 6); }
print(hp.join(" "));
try { parallel_for (auto& d : drops) { loot.seed(d); } } catch (e) { print(e); }

class Monster { int hp = 10; random brain; }                       // or a field: each monster's own generator
array<Monster> pack = [];
for (int i = 1; i <= 3; ++i) { Monster m; m.brain.seed(i); pack.push(m); }
parallel_for (auto& m : pack) { m.hp -= m.brain.next_int(1, 6); }
print(to_string(pack[0].hp) + " " + pack[1].hp + " " + pack[2].hp);
output
51 17 31 91 93 79 47 67
61
5 9 5
parallel_for: random::seed cannot be called on captured 'loot' in a parallel body (seed before the loop: each element draws its own stream from it) at 11:45
5 9 5

Reseeding a captured generator is refused before any element runs, and so is shuffling captured state with it (parallel_for: argument passes enclosing state 'deck' by reference (captured reads are read-only)). Any other captured object keeps the captured-object rules of Parallel Loops: its methods that write no part of it run, and one that writes it is refused.

crypto:: for secrets​

Anything an attacker must not guess comes from crypto::. crypto::random_bytes(n) returns n bytes from the operating system's secure generator (BCryptGenRandom on Windows, getrandom on Linux and Android, arc4random_buf on Apple platforms and the BSDs); crypto::random_hex(n) writes n of them as 2n hex digits, a ready-made token; crypto::random_int(lo, hi) is a secure, unbiased int over lo..hi. None of them takes a seed, and none ever will: a seed is a value someone can learn or guess, and every number drawn from it can then be predicted. Use a seeded random where the same numbers must come back, and crypto:: where they must not.

crypto::sha256(s) and crypto::sha512(s) are the FIPS 180-4 digests of the bytes of s (a utf8_string's UTF-8 bytes) in lowercase hex; crypto::sha1 and crypto::md5 exist only to read and write formats that name them and protect nothing. Each has a _bytes twin that returns the raw digest as a byte string (crypto::sha256_bytes, for one). crypto::sha256_file(path) streams a file through SHA-256; it needs the filesystem privilege and raises read_file's Cannot read file '<path>': <reason>. crypto::hmac_sha256(key, message) and crypto::hmac_sha512 are RFC 2104 HMACs (hex, with _bytes twins), and crypto::equals(a, b) compares two strings in time that depends only on their length, the comparison a MAC or token check needs.

jaiscript
print(crypto::sha256("abc"));
print(crypto::sha512("abc").size(), " ", crypto::md5("abc"), " ", crypto::sha256_bytes("abc").size());
string signature = crypto::hmac_sha256("server secret", "user=7&expires=1700000000");
print(signature);
print(crypto::equals(signature, crypto::hmac_sha256("server secret", "user=7&expires=1700000000")));

string token = crypto::random_hex(16);          // 32 hex digits from the operating system
print(token.size(), " ", crypto::random_bytes(8).size(), " ", crypto::random_int(1, 6) <= 6);

string stored = "$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a";   // written by PHP's password_hash
print(crypto::password_verify("rasmuslerdorf", stored), " ", crypto::password_verify("wrong", stored));
string mine = crypto::password_hash("correct horse", 10);
print(mine.substr(0, 7), " ", mine.size(), " ", crypto::password_verify("correct horse", mine));
output
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
128 900150983cd24fb0d6963f7d28e17f72 32
89416909585f136bc7810bac6add16fadd8dc8c6361edba9c4983caf0f5afedf
true
32 8 true
true false
$2y$10$ 60 true

Passwords​

crypto::password_hash(password) is bcrypt in the form PHP's password_hash writes: $2y$, a two-digit cost, 22 characters of a fresh 16-byte salt and 31 of hash, 60 characters in all. The cost is 12 unless a second argument gives one from 4 to 31 (crypto::password_hash cost must be 4 to 31); each step doubles the work, and cost 12 takes about a quarter of a second. crypto::password_verify(password, hash) checks a $2y$, $2b$, $2a$ or $2x$ hash (including $2a$'s countermeasure for the old sign-extension bug, as PHP computes it) and answers false for any other text, so hashes PHP stored in a user table verify unchanged, and one made here verifies in PHP.

As in PHP, only a password's first 72 bytes count: a longer one hashes like its first 72 bytes. A password holding a NUL byte cannot be hashed (crypto::password_hash: the password contains a NUL byte), since bcrypt reads only up to it, and verifying one is false. Both compare the hash in constant time.

In parallel loops​

Every crypto:: function runs in a parallel body except crypto::sha256_file, which reads a file and is refused before any element runs (parallel_for: crypto::sha256_file cannot be called in a parallel body (it reads a file)). The secure generator is safe on any thread; its numbers are unpredictable in a parallel loop just as in a serial one.

Where to next​

The full signatures list every method of random and every crypto:: function; Parallel Loops has the rest of what a parallel body may do, and Serialization how a save file carries a generator.