Beyond the Random Class: Cryptographic Randomness in .NET 6+

Learn how to generate cryptographically secure random numbers in .NET applications using the RandomNumberGenerator class. Discover the importance of true randomness for security-sensitive tasks and explore best practices for generating unpredictable values.

We live in a naturally unpredictable world. Shuffling a deck of cards, picking lottery numbers, and weather patterns are all random. Software often needs to mimic this unpredictability, such as spawning enemies at random intervals in a game, selecting a random sample of data for training a machine learning model, or generating a random OTP code when authenticating a user. Computers, however, are deterministic machines. Given a particular input, the computer will always produce the same output. This makes replicating the unpredictability we live in a challenge in code.

C# offers a couple of APIs for generating random numbers, with the Random class being a common choice. However, while useful in many scenarios, its implementation isn't always appropriate, especially in security contexts where its predictability can be exploited. In those cases, alternatives like the RandomNumberGenerator, which offer a higher degree of unpredictability, are better suited.

In this article, you will learn how the Random class functions in C#, when it's appropriate to use, when and why it's not always suitable, and what alternatives to consider.

Why Computers Aren't Truly Random

Computers follow a strict set of instructions to perform a task. Given the same input, the process will yield the same output. This is great for computers, and it's what's needed for them to perform computations accurately.

When it comes to randomness, the computer has to cheat by relying on algorithms that generate seemingly random numbers. These algorithms are called Pseudo-Random Number Generators (PRNGs) and consist of mathematical formulas to produce a sequence of numbers that appear random but are still deterministic. PRNGs start with a seed value, which is the starting point for the sequence generation. Given the same seed value, a PRNG will always produce the same sequence of numbers. While not completely random, this predictability can sometimes be helpful where reproducibility is desired, like in unit tests or where reproducing the sequence carries little risk.

Animation illustrating how PRNGs can repeat the same sequence with the same seed.

However, there are cases where you don't want this reproducibility. If an attacker can figure out the seed used to generate the number sequence, they can "predict" the next numbers. In specific contexts, like security, this can result in a system getting compromised. For example, if you use a PRNG to generate OTP codes for authentication, an attacker could compromise the system by figuring out the seed and generating the same codes. This is why true randomness, which is unpredictable, is essential for security-sensitive tasks.

Did you know?
Cloudflare, one of the world's leading CDN providers, generates completely random encryption keys using a wall of 100 lava lamps. A camera mounted to capture all the lava lamps takes regular pictures of the lamps and sends them to Cloudflare servers, where the image pixels are used to generate the next encryption key. Read more about it on their blog.

The Random Class: "Good-Enough" Randomness

The Random class is the de facto way of generating random numbers in most applications. It's a PRNG that's been around since the beginning of the .NET Framework. Although its underlying implementation has changed over the years, its API has remained relatively unchanged. It's intuitive, efficient, and works well in most cases.

Generating Random Integers

To use it, all you need to do is create a new instance of the class and then get the next random numbers:

var random = new Random();
Console.WriteLine(random.Next());
Console.WriteLine(random.Next());

// Output:
// 1472324096
// 2138638347

The Next() method generates a non-negative random integer. You can also pass in parameters to restrict the result if necessary:

var random = new Random();

// Generate random integer no larger than 9
Console.WriteLine(random.Next(10));

// Generate random integer between 10 and 19 (inclusive)
Console.WriteLine(random.Next(10, 20));

// Output:
// 7
// 14

When using the overloads, the start number is inclusive (if specified), and the end number is exclusive.

The Random class also has methods to generate random bytes, doubles, longs, and even pick random items from an array.

Seeding the Generator

You'll notice that no seed value was specified when instantiating the Random class above. When one isn't specified, .NET will generate the seed for you. How that seed is generated has changed over the years. In .NET Framework, the current time is used to create the seed. However, .NET Core and 5+ have improved on this by making an interop call to the operating system to generate a random seed using hardware, which is much less predictable.

You can pass in a seed using one of the constructor overloads. This is useful if you want to control the sequence of numbers generated. The snippet below demonstrates this:

// Use 78 as the seed
var random = new Random(78);

Console.WriteLine(random.Next());
Console.WriteLine(random.Next());

// Output:
// 1020951901
// 1511780237

You should get the same output as the snippet if you run the code below.

When To Use It

The Random class is simple and very efficient since all the numbers are generated by software (and not hardware, as you'll see in the next section). The generated numbers are spread across your desired range and will seem random enough for most applications. An end user shouldn't pick up any patterns.

You'll want to avoid it, though, if being able to predict values will compromise the security of your application or the integrity of your data. For example, generating password salts, nonces, and OTPs should not use the Random class since predicting those values will be disastrous. Similarly, you might not want to use the Random class for applications like gambling and picking winners for a draw since predicting the numbers could ruin the integrity of the game.

Warning on Instantiation and Thread Safety

The official Microsoft documentation advises not to re-instantiate the Random class every time you need a number. This makes sense, especially if you're working with .NET Framework, since seeds are generated using the system time. Two generators can have the same seed value if they are instantiated close together.

However, even if you're working in modern .NET (i.e., .NET 5 and up), re-instantiating the generator doesn't make sense. The generator is guaranteed to choose from a range of numbers with equal probability, meaning no number is more likely to be generated than another. This is good! But you lose that guarantee if you re-instantiate the Random class every time.

Also, keep in mind that the Random class is not thread-safe. To overcome this challenge, the documentation demonstrates how you can use locks to make sure the Random object is never called illegally.

The RandomNumberGenerator Class: Cryptographically Secure Randomness

Since computers are deterministic, generating truly random numbers is difficult. You need to incorporate entropy into the algorithm. This entropy typically comes from something in real life that exhibits random, unpredictable behaviour. Cloudflare gets entropy from a wall of lava lamps; C# gets entropy from the operating system, which, in turn, relies on underlying hardware for randomness. You can access this more secure random number generator using the RandomNumberGenerator class.

Microsoft recommends using the static implementation of the class, which will be demonstrated in this article.

Generating Random Integers

Unlike the Random class above, you need to specify the upper bound for the number you want to generate with the RandomNumberGenerator class. Another overload also lets you choose the lower bound:

// Get a cryptographically random number between 0 and 9
Console.WriteLine(RandomNumberGenerator.GetInt32(10));

// Get a cryptographically random number between 10 and 19
Console.WriteLine(RandomNumberGenerator.GetInt32(10, 20));

// Output:
// 8
// 16

Notice how the output is similar to our previous example, where we used the Random class. However, these are far less predictable, and because of entropy, it's practically impossible to repeat the sequence of numbers generated.

Generating Random Strings (.NET 8+)

The release of .NET 8 introduced several new helper methods to the RandomNumberGenerator class. One such method lets you provide a source string and desired length and then generates a random string of that length using the characters in the source string:

var availableCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";

// Get a cryptographically random string 20 characters long
Console.WriteLine(RandomNumberGenerator.GetString(availableCharacters, 20));

// Output:
// "5LZFSEVZTPI7LTFTPIJJ"

This is useful if you need to create passwords or OTPs.

Generating Random Hexadecimal Strings (.NET 8+)

The hexadecimal number system lets you represent numbers in base 16. It does this using characters 0-9 and A-F. From .NET 8 and up, you can generate random hexadecimal numbers using the GetHexString static method on the RandomNumberGenerator class:

// Get a cryptographically random hex string 5 characters long with 
// uppercase characters
Console.WriteLine(RandomNumberGenerator.GetHexString(5));

// Get a cryptographically random hex string 8 characters long with
// lowercase characters.
Console.WriteLine(RandomNumberGenerator.GetHexString(8, lowercase: true));

// Output:
// "F23BB"
// "7c92dfa5"

When To Use It

After seeing RandomNumberGenerator in action, you might wonder why you would use the Random class. But wait! As great as the RandomNumberGenerator class is, it has additional overhead. Whenever it generates a number, it calls the operating system to add entropy. This call does make the RandomNumberGenerator much slower, which you'll see in this benchmark.

The additional overhead means you should only use the RandomNumberGenerator when you actually need cryptographically random numbers. For any other use case, stick to the Random class.

Conclusion

This article explored the intricacies of generating random numbers in code, focusing specifically on the Random and RandomNumberGenerator classes in C#. You first learned how computers are deterministic and need to use PRNGs to generate seemingly random numbers. You then saw how to pick random numbers from the Random class, a PRNG generator. While simple and efficient, the Random class has limitations, especially regarding cryptographic operations.

You were then introduced to the RandomNumberGenerator, which uses entropy from the underlying operating system to generate cryptographically secure numbers which are practically impossible to predict. The article also demonstrated newer methods added to the class in .NET 8 and up, simplifying generating random strings and hexadecimal numbers.

As I looked through the source code for these classes, it's clear that very clever engineers have done much of the heavy lifting for us. However, it's still essential to understand how these classes work to use them effectively in your code base.

If you have any comments or feedback, please leave a comment below.