HAProxy:
Articles:
🇬🇧 Go random : Â« math Â» vs Â« crypto Â»  (2024)
Electronique:
Projects:
Pet projects:
Archives :
Divers:

Article Illustration

🇫🇷 Version française disponible ici

Using math/rand with current nanosecond vs crypto/rand in Go

In the context of generating secrets such as session tokens or passwords, basic security concepts require a certain randomness that prevents an attacker from guessing the secret, even if they know its generation parameters.

Of course, we will rather use "crypto/rand" because it is labeled "crypto", or because it is recommended in the CISO's secure development charter.

Entropy

To measure the strength of a secret, we use entropy. Very roughly, entropy measures real randomness or disorder. The higher it is, the greater the disorder. To calculate the entropy of a password, we take all possible character combinations achievable for a given password size. This will give a number of combinations. We then express this number of combinations in bits. Entropy is expressed in bits because a bit is the smallest measurable unit of data.

To convert a number of possibilities into bits, we need to solve the equation 2^x = y where "y" is the number of possibilities and "x" the number of bits. The answer is therefore found with a base-2 logarithm (log_2), which is log_2(y) = x.

So, a password with a length of 2 elements where each element is a digit can take 100 different values, from "00" to "99", which is an entropy of 6 bits.

If the password policy allows alphabetic characters, the user can use common names or proper names, such as "anne", "car", ... This usage considerably reduces entropy. Indeed, dictionary attacks assume that most users will use common names, and will precompute passwords using these common names as a priority. Consequently, this usage reduces the number of possible combinations and therefore the entropy.

At the end of 2022, the values recommended by CNIL (Passwords: a new recommendation to master your security) for generating a password are:

  • Simple password, without additional control: 80 bits
  • Password with controls (number of failed attempts, ...): 50 bits

Pseudo-random generators

"math/rand" is not a good candidate for generating secrets because it is a pseudo-random generator. This means it generates a sequence of values that appears random but is absolutely not. Indeed, if we know the seed and the algorithm we can predict the sequence. It is therefore deterministic and predictable. The seed provided to "math/rand" is a signed 64-bit integer. If this integer is random, the entropy of everything that will be generated by "math/rand" will be 64 bits. It is very easy to ensure this behavior with the code below. It will be a matter of varying the seed and comparing the results.

package main

import "math/rand"
import "fmt"

func main() {
   var rng *rand.Rand

   rng = rand.New(rand.NewSource(23))
   fmt.Printf("%d %d %d %d\n", rng.Int(), rng.Int(), rng.Int(), rng.Int())

   rng = rand.New(rand.NewSource(23))
   fmt.Printf("%d %d %d %d\n", rng.Int(), rng.Int(), rng.Int(), rng.Int())
}

As a seed, one might think that the current date in nanoseconds is sufficiently random to generate a secret. Indeed, there are many nanoseconds in a second and the imprecision of the system clock adds randomness. We can think that it will be very difficult for the attacker to guess the generation date of a secret, especially in nanoseconds. The attacker can frame the generation date in time to reduce the possibilities, but a randomly generated password will be difficult to frame in time. Let's retain a reasonable framing of one month, indeed the attacker will be able to estimate in which month a user subscribed to a service. To be honest, I retain the value of one month because it suits me for the following :-). In practice, the attacker could be more precise.

We therefore have an entropy of log_2(10^9 \cdot 3600 \cdot 24 \cdot 30) = 51bits. In theory, this is compatible with CNIL rules.

Using nanoseconds

In Go, we can use the "time.UnixNano" function to get nanoseconds. It would be good to ensure the entropy of the clock provided in Go. Since the time system works very well, we won't have doubts about the precision of the second. So let's look at what happens with nanoseconds.

package main

import "time"
import "fmt"

func main() {
	var i int

	for i = 0; i < 10000; i++ {
		fmt.Printf("%09d\n", time.Now().Nanosecond())
	}
}

On my Mac "Sonoma" with "2.3 GHz Quad-Core Intel Core i7", the result looks like this:

...
504629000
504630000
504631000
504633000
504634000
504635000
504636000
504638000
504639000
504640000
...

We clearly see that these are microseconds and not nanoseconds. The entropy is divided by 1000 to reach: log_2\left(\frac{10^9 \cdot 3600 \cdot 24 \cdot 30}{1000}\right) = 41bits. This is not sufficient.

On a "Debian 4.9" server with "Intel(R) Atom(TM) CPU C2338 @ 1.74GHz". The result is as follows:

...
570532701
570544820
570560828
570574580
570586520
570598771
570605839
570640806
570668058
570682145
...

So, the reality of entropy based on date will depend on an attacker's ability to frame the secret generation date and the OS/hardware on which we work.

What does "crypto/rand" do

"crypto/rand" is a true random number generator defined for randomness compatible with cryptography (CSPRNG). To ensure randomness, it uses entropy sources provided by the operating system to generate truly random and unpredictable numbers. We therefore cannot determine in advance the value it will generate. "crypto/rand" is therefore preferred for cryptographic use, as it will improve the security of an application that uses it to generate its secrets. However, it is more time-consuming than "math/rand".

"math/rand" will be used for situations where pseudo-randomness is sufficient such as generating test data or simulations. The pseudo-random property based on a seed can be interesting for procedural generation of complex environments like Minecraft worlds, where the same seed generates the same world.

Recommendation

Rather than making an assumption about an attacker's ability to guess a date and issuing a specification on the hardware to use for the software to work, it is simpler to use an unpredictable source of randomness, which is provided by "crypto/rand".