Random Numbers & Crypto
Randomness is a value you own: a random object holds one generator, and nothing else in the program can move it. There is no hidden global stream, as there is with C's rand.
- A seeded
randomdraws the same numbers on every platform, compiler and backend: what replays, tests and rollback netcode need. Unseeded, it starts from 32 bytes of the operating system's secure generator, so every run differs. - A
randomis a value likeint: a copy holds the same state and replays the same draws, so a snapshot is an assignment and a rollback is another. - Secrets (tokens, keys, salts, passwords) come from
crypto::, which can never be seeded.
A generator you own
next_int(lo, hi) includes both bounds. next_float() is in [0, 1) and next_float(lo, hi) in [lo, hi). 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 (signatures). An inverted range, a non-finite float bound, an empty pick, a shuffle of a non-array and a negative byte count raise (texts). 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.
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_float(), " ", r.next_float(-1.0, 1.0), " ", r.next_bool());
array<string> deck = ["ace", "king", "queen", "jack"];
r.shuffle(deck);
print(deck.join(" "), " / ", r.pick(deck), " / ", r.next_bytes(4).size(), " bytes");
try { r.next_int(6, 1); } catch (e) { print(e); }true
1 0.782103772866755 -0.8558901198740734 false
king queen ace jack / queen / 4 bytes
random::next_int range is inverted (max < min)Seeds, replays and rollback
random r(seed);,random r = random(seed);andr.seed(seed)all start the same stream. A float seed converts as everyintparameter of a bound function does: toward zero, saturating at theintrange, NaN as 0.- Every copy is independent: a variable, a parameter, a field, an array or map element.
[r, r], also as anarray<random>, holds two copies ofr's state, so each element andritself draw the same next number. r.stateis the generator's whole state as 64 lowercase hex digits. Storing one resumes that stream exactly; anything but 64 hex digits, or all zeros, raisesrandom::state expects 64 hex digits, not all zero.to_json,from_json,to_base64andfrom_base64carry the state, so a save file restores a generator mid-stream. A host holds the same generator asjai::stdlib::random(<jaiscript/stdlib/random.hpp>), whosesaveandloadwrite the samestatethrough the C++ archives (Serialization).
random a(7);
random b = random(7);
random c(1);
c.seed(7);
print(a.next_int(0, 99), " ", b.next_int(0, 99), " ", c.next_int(0, 99));
var two = [a, a]; // two copies of a's state, not a twice
print(two[0].next_int(0, 99), " ", two[1].next_int(0, 99), " ", a.next_int(0, 99));
random saved = a; // a snapshot
int before = a.next_int(0, 1000);
a = saved; // roll back
string json = to_json(saved);
print(before == a.next_int(0, 1000), " ", from_json(json).next_int(0, 1000) == before, " ", from_json(json).state == saved.state);
print(json);70 70 70
27 27 27
true true true
{"$type":"random","$val":{"state":"3a5859de077bf1a98f5ee2ecf234241c35275021661965d57675391cadf3b9a9"}}The algorithm, exactly
Every formula is part of the language, so a seed draws the same bits everywhere. The reference gives the bit use of next_float(), next_bool() and next_bytes.
The generator is xoshiro256** 1.0 seeded through splitmix64; next_int(lo, hi) is Lemire's method and shuffle is Fisher–Yates. Each step's definition: random generator algorithm.
In parallel loops
Parallel Loops has the full treatment and the refusals. In short:
- A captured
randomgives each element its own stream, seeded from one draw the statement takes from the captured generator. The results are the same at every worker count but are not the numbers the serial loop would draw, and the generator moves one draw per statement. Reseeding it in the body, or shuffling captured state with it, is refused before any element runs. - A generator the element owns (an
array<random>zipped beside the data, or a field of the element's class) and a seeded one the body makes draw as they would anywhere, so they give the serial loop's numbers. Two elements may not hold one generator object. - An unseeded
random q;made in the body andcrypto::are admitted and are not reproducible at all.
random loot(5);
random serial = loot;
array<int> drops = [0, 0, 0, 0];
array<int> in_order = [0, 0, 0, 0];
for (auto& d : in_order) { d = serial.next_int(1, 100); }
parallel_for (auto& d : drops) { d = loot.next_int(1, 100); }
print(drops.join(" "), " / serial: ", in_order.join(" "));51 17 31 91 / serial: 29 61 65 83crypto:: for secrets
Use a seeded random where the same numbers must come back, and crypto:: where an attacker must not guess them. Nothing here takes a seed: every number drawn from a seed someone learns can be predicted. The reference lists every function; in outline:
- Secure randomness:
crypto::random_bytes(n),crypto::random_hex(n)(2n hex digits, a ready-made token) andcrypto::random_int(lo, hi)read the operating system's generator (BCryptGenRandom on Windows, getrandom on Linux and Android, arc4random_buf on Apple platforms and the BSDs). - Digests:
crypto::sha256(s)andcrypto::sha512(s)hash the bytes ofsto lowercase hex.crypto::sha1andcrypto::md5exist only to read and write formats that name them and protect nothing.crypto::sha256_file(path)streams a file and needs the filesystem privilege. - MACs:
crypto::hmac_sha256(key, message)andcrypto::hmac_sha512. Check one withcrypto::equals(a, b), which takes time that depends only on the length. - Every digest and MAC has a
_bytestwin that returns raw bytes (crypto::sha256_bytes). - Every function runs in a parallel body except
crypto::sha256_file(parallel_for: crypto::sha256_file cannot be called in a parallel body (it reads a file)).
print(crypto::sha256("abc"), " ", crypto::sha256_bytes("abc").size());
string signature = crypto::hmac_sha256("server secret", "user=7&expires=1700000000");
print(crypto::equals(signature, crypto::hmac_sha256("server secret", "user=7&expires=1700000000")));
print(crypto::random_hex(16).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));ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 32
true
32 8 true
true false
$2y$10$ 60 truePasswords
Passwords hash and verify the way PHP's do, so hashes move between the two unchanged.
crypto::password_hash(password)writes bcrypt as PHP'spassword_hashdoes:$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. Each step doubles the work; cost 12 takes about a quarter of a second.
crypto::password_verify(password, hash)checks a$2y$,$2b$,$2a$or$2x$hash (with$2a$'s countermeasure for the old sign-extension bug, as PHP computes it) in constant time, and answers false for any other text.- As in PHP, only a password's first 72 bytes count. A password holding a NUL byte cannot be hashed (bcrypt reads only up to it), and verifying one is false.