How do I generate a secure random token, nonce or salt in EK9?

← Advanced Type System · Ref: Q1388

SecureRandom produces cryptographically secure random OCTETS. It has exactly one operation:

  salt <- SecureRandom().nextBytes(16)   // Integer count -> Bytes

A TOKEN IS OCTETS RENDERED AS TEXT

  token <- SecureRandom().nextBytes(32).toBase64()
  hexId <- SecureRandom().nextBytes(16).toText("hex")

There is deliberately no next() returning an Integer. Reducing random numbers into a range by hand is how modulo bias creeps into tokens, and octets rendered as Base64 or hex are what every protocol actually wants.

IT IS DELIBERATELY NOT A Random
SystemRandom and SeededRandom both implement the Random trait. SecureRandom does NOT. If it did, any function taking a Random would accept a SeededRandom just as happily - and a seeded generator produces the SAME sequence every run, so a nonce or salt made from it is reproducible by anyone who knows the seed. Keeping SecureRandom outside the trait means a function that needs unpredictable octets declares a SecureRandom parameter, and passing it a SeededRandom is a COMPILE ERROR (E06270), not a code-review catch.

NO SEED, ON PURPOSE

There is no SecureRandom(seed) constructor. The generator seeds itself from the operating system's entropy source; there is no way to make it reproducible, and that is the whole point.

TRI-STATE

An unset or negative count gives UNSET Bytes - never a substitute length. A count of 0 gives EMPTY Bytes, which are SET: asking for nothing and getting nothing is a well-defined answer.

TESTING CODE THAT USES IT

There are no test vectors for a secure generator, by definition. Test the PROPERTIES instead: the requested length comes back, successive draws differ, two generators differ. If a test needs repeatable values, the code under test wanted a Random, not a SecureRandom - the two answer different questions.

See Q1383 for Bytes, Q1386 for Digest and Q1387 for HMAC. Use 'ek9 -h SecureRandom' for the full API.

Example

defines module qa.securerandom.usage

  defines function

    //Declaring SecureRandom here is the safety property: a SeededRandom will not compile at the call site.
    newSalt() as pure
      ->
        generator as SecureRandom
      <-
        rtn as Bytes: generator.nextBytes(16)

  defines program

    SecureRandomUsage()
      stdout <- Stdout()
      generator <- SecureRandom()

      //A salt: exactly the number of octets asked for.
      salt <- newSalt(generator)
      stdout.println(`salt length: ${length salt}`)

      //A token is octets rendered as text - 32 octets is 44 characters of Base64.
      token <- generator.nextBytes(32).toBase64()
      stdout.println(`token length: ${length token}`)

      //Or as hex - two characters per octet.
      hexId <- generator.nextBytes(16).toText("hex")
      stdout.println(`hex id length: ${length hexId}`)

      //Successive draws differ, and so do two generators - there is no seed for them to share.
      stdout.println(`draws differ: ${generator.nextBytes(32) <> generator.nextBytes(32)}`)
      stdout.println(`generators differ: ${SecureRandom().nextBytes(32) <> SecureRandom().nextBytes(32)}`)

      //Asking for nothing gives EMPTY Bytes - which are SET. Empty is not unset.
      nothing <- generator.nextBytes(0)
      stdout.println(`empty is set: ${nothing?}`)
      stdout.println(`empty length: ${length nothing}`)

      //An unset or negative count gives UNSET - never a substitute length.
      stdout.println(`unset count: ${generator.nextBytes(Integer())?}`)
      stdout.println(`negative count: ${generator.nextBytes(-1)?}`)

Common mistakes

E06270 — SecureRandom is deliberately NOT a Random, so no Random - seeded or otherwise - can stand in for it. That is the point: a seeded generator gives the same salt every run. The compiler rejects the substitution at the call site.

Incorrect:

salt <- newSalt(SeededRandom(42))

Correct:

salt <- newSalt(generator)

E50060 — SecureRandom only produces octets. There is no next() returning an Integer; render the octets as Base64 or hex for a token, or use SystemRandom if you genuinely wanted a non-cryptographic number.

Incorrect:

token <- generator.next()

Correct:

token <- generator.nextBytes(32).toBase64()

E50060 — There is no seeded constructor - a secure generator seeds itself from the operating system and cannot be made reproducible. If you need repeatable values (a simulation, a test), you wanted SeededRandom.

Incorrect:

generator <- SecureRandom(42)

Correct:

generator <- SecureRandom()
Other ways to ask this
  • What is the EK9 equivalent of java.security.SecureRandom?
  • How do I generate cryptographically secure random bytes?
  • Why can I not pass a SeededRandom where a SecureRandom is expected?
  • How do I make a session token or an API key?

Coming from another language?

Java: java.security.SecureRandom EXTENDS java.util.Random, so any method taking a Random accepts either, and new Random(42) passed where a SecureRandom was intended type-checks and yields the same nonce every run. Python: secrets.token_bytes / token_urlsafe are right, but random.getrandbits looks identical at the point of use and nothing in a signature distinguishes them. Go: crypto/rand.Read and math/rand.Read share a shape, so an io.Reader parameter accepts either. Node: crypto.randomBytes is correct; Math.random is not cryptographic and is the one people reach for. EK9: a separate type outside the Random trait, octets only, no seed, and the wrong generator is a compile error.

Keywords: securerandom, token, salt, csprng, cryptography, unpredictable, nonce, entropy, random, seededrandom