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 random draws 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 random is a value like int: 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.

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_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); }
output
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); 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.
  • Every copy is independent: a variable, a parameter, a field, an array or map element. [r, r], also as an array<random>, holds two copies of r's state, so each element and r itself draw the same next number.
  • r.state is the generator's whole state as 64 lowercase hex digits. Storing one resumes that stream exactly; 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 the state, so a save file restores a generator mid-stream. A host holds the same generator as jai::stdlib::random (<jaiscript/stdlib/random.hpp>), whose save and load write the same state through the C++ archives (Serialization).
jaiscript
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);
output
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 random gives 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 and crypto:: are admitted and are not reproducible at all.
jaiscript
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(" "));
output
51 17 31 91 / serial: 29 61 65 83

crypto:: 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) and crypto::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) and crypto::sha512(s) hash the bytes of s to lowercase hex. crypto::sha1 and crypto::md5 exist 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) and crypto::hmac_sha512. Check one with crypto::equals(a, b), which takes time that depends only on the length.
  • Every digest and MAC has a _bytes twin 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)).
jaiscript
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));
output
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 32
true
32 8 true
true false
$2y$10$ 60 true

Passwords​

Passwords hash and verify the way PHP's do, so hashes move between the two unchanged.

  • crypto::password_hash(password) writes bcrypt as PHP's password_hash does: $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.