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.
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.
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.