47. Cryptographic random numbers

This is much more complicated that you might imagine. (In fact, it's surprising that Microsoft didn't add methods to generate values within a range to its cryptographic random number generator so you wouldn't have to.) To understand why this is hard and to learn how to handle the difficulties, take the following discussion slowly. If you find that you’re lost, you may want to start over.

The .NET Framework's RNGCryptoServiceProvider class generates cryptographic random values. It is fairly easy to use but it has a catch—it only generates bytes. If you want some other kinds of data, such as double or int that lies between two bounds, then you need to convert the bytes accordingly.

To make the discussion easier, I'll use the notation [A, B) to mean the range of numbers between A and B, including the A value (the [ means include the end value) and not including the B value (the ) means exclude the end value). I'll also work through a very simplified example that uses numbers that are much smaller than those typically used by real random number generators.

One simple method for generating numbers within a range is to generate a random number and then take that number mod the number of values that you want to generate. For example, suppose your random number generator creates numbers in the range [0, 9] and you want to pick a value in [0, 7]. Now suppose your random number generator gives you 5. The value 5 % 8 = 5, so 5 is the number that you use. I'm using 8 as the modulus because we want one of eight values between 0 and 7 inclusive. Taking the result mod 8 ensures that the result lies between 0 and 7. So far so good.

Next, suppose the generator produces 9. In that case, 9 % 8 = 2, so 2 is the number that you would use.

This method is easy to calculate, but it also has a big problemthe resulting values are not evenly distributed. In this example, there's only one way you can generate a 7, namely the generator produces 7. However, there two ways that you can generate the value 0—when the generator produces 0 or 8. That means you're twice as likely to end up with 0 as 7.

The following table shows all of the possible results created by the generator and the final numbers that they produce:

Generated

0

1

2

3

4

5

6

7

8

9

Result

0

1

2

3

4

5

6

7

0

1

 

You can see in the table that the results 0 and 1 appear twice while the other results appear only once.

Real random number generators produce much larger numbers, so the problem isn't as large. For example, if the random number generator produces values between 0 and 99 and we still map them into the range [0, 7], then there are 13 ways to produce some numbers. For example, you will get a result of 0 if the generator produces 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, or 96. However, there are only 12 ways to get some other values. For example, you get the value 7 if the generator produces one of the values 7, 15, 23, 31, 39, 47, 55, 63, 71, 79, 87, or 95. That's a lot better than producing some values twice as often as others, but it's still a problem, particularly for a CRNG.

The goal of a CRNG is to ensure that an attacker cannot guess the next number in a sequence with even a slightly better than random chance of success.

One way to fix this problem is to discard any values that appear at the end of the range of generated values. To see how that works, let's go back to the example where you need to pick a value in [0, 7] and the generator produces values in [0, 9]. In that case, you simply discard any generated value larger than 7 and try again. You may need to try more than once to pick a number, but you probably won't need more than a few tries.

The following table shows the new results:

Generated

0

1

2

3

4

5

6

7

8

9

Result

0

1

2

3

4

5

6

7

Retry

Retry

 

The case where the generator produces values in [0, 99] is similar; you just discard any value above 95.

There's one more technique that you need to use to generate values within a range if the lower bound is not zero. In that case, subtract the upper limit from the lower limit to see how many values you need to consider. Pick a random number between zero and the number of values that you need, and add the result to the lower bound.

I know this is confusing, so let's look at an example. Suppose you want to pick a value in [10, 17]. That range includes eight values, so use the previous method to pick a value in the range [0, 7], which also holds eight values. Then add the result to the lower bound 10 to get the final number in the range [10, 17].

The following code shows how the example solution implements this technique to pick a random number in the range [minValue, maxValue). Notice that the range does not include the upper limit, maxValue. That makes the method more consistent with the  Random class's Next method and makes the math slightly easier:

// Generate a cryptographically secure long in [minValue, maxValue).
public static long NextLong(long minValue, long maxValue)
{
// The biggest value we can create with 16 bytes.
BigInteger biggest = BigInteger.Pow(2, 8 * 16) - 1;

// Calculate the number of values in [maxValue, minValue).
long numValues = maxValue - minValue;

// The amount of unused space at the end of the biggest range.
BigInteger numUnused = (biggest + 1) % numValues;

// The largest usable value.
BigInteger maxUsable = biggest - numUnused;

// Generate random BigIntegers until we get one between
// 0 and maxUsable.
byte[] bytes = new byte[16];
for (;;)
{
// Get a random BigInteger.
BigInteger rand = RandomUBigInteger(16);

// If the value is within the allowed range, use it.
if (rand <= maxUsable)
return (long)(minValue + (rand % numValues));
}
}

In order to produce long integer values, the method must use a larger data type than long to perform its calculations. (You can't generate numbers in [0, 7] if the random number generator only produces values in [0, 5].) You might like to use the double data type, but it doesn't have enough resolution to represent all long values. For example, consider the following statement and result from Visual Studio's Immediate Window:

(long)(double)1234567890123456789
1234567890123456768

The value lost precision when it was stored in a double, so when it was turned back into a long, the last two digits were modified. Fortunately, the .NET Framework has a data type that can hold very large values and that has perfect integer precision—BigInteger.

To use the BigInteger data type, give your program a reference to the System.Numerics library. You may also want to add the following using directive to your code:

using System.Numerics;

The method sets the biggest variable to the largest value that it might generate. This is analogous to the value 9 in the example that generated numbers in [0, 9] and then converted them into the range [0, 7]. This example will use 16-byte values, so those values use 16 * 8 bits. The largest value that you can represent in 16 bytes is 216*8 – 1 when every bit is 1. (That's big enough to hold any long value because the long data type also uses 16 bytes.)

Next, the method calculates the number of values in the desired range.  For example, if the range is [10, 17), then the number of values is 7.

The code then calculates the number of generated values in the range [0, biggest] that will be unused. Those are the values for which the method must try again. Earlier, when we wanted a value in [0, 7], if the random number generator produced 9, we tried again. In that example, biggest was 9 (the generator produced values in [0, 9]) and numValues was 8 (we wanted numbers in [0, 7]), so numUnused would be (9 + 1) % 8 = 2. That means we ignore the two largest values if we generate them. If you look back at the earlier table, you'll see that we retried if we generated the two largest values 8 or 9.

Next, the code uses numUnused to calculate the largest value that the method will use. For the previous example, numUnused is 2 and biggest is 9, so the largest usable value is 9 – 2 = 7, which matches the preceding table.

With all that set up, the method is finally ready to pick a number. It creates a byte array to hold 16 bytes and then enters an infinite loop.

Inside the loop, the code calls the RandomUBigInteger method described shortly to generate a BigInteger defined by 16 cryptographically secure random bytes. If the value is less than or equal to the largest usable value, the method adds it to minValue and returns the result. If the value is bigger than the largest usable value, the method continues its loop and tries again.

The following code shows the RandomUBigInteger method:

// A cryptographic pseudorandom number provider.
private static RNGCryptoServiceProvider
Cprng = new RNGCryptoServiceProvider();

// Return a cryptographically secure random unsigned BigInteger
// with the indicated number of bytes.
private static BigInteger RandomUBigInteger(int numBytes)
{
// Get the random bytes.
byte[] bytes = new byte[numBytes];
Cprng.GetBytes(bytes);

// Convert the bytes into an unsigned value;
BigInteger result = 0;
BigInteger factor = 1;
for (int i = 0; i < numBytes; i++)
{
result += factor * bytes[i];
factor *= 256;
}
return result;
}

The code creates a static CPRNG named Cprng at the class level so it is available inside all of the class's methods.

The RandomUBigInteger method creates an array big enough to hold the desired number of bytes and then calls the Cprng object's GetBytes method to fill the array with random bytes.

The method then uses the bytes to create a result by multiplying the values of the bytes by powers of 256. This is similar to the way that you calculate the value of a decimal number. For example, the decimal value 1729 is 9 * 100 + 2 * 101 + 7 * 102 + 1 * 103. Similarly the bytes create the value byte0 * 2560 + byte1 * 2561 + byte2 * 2562 + ... + byteN * 256N.

After it finishes processing the bytes, the method returns the resulting BigInteger.

That ends the most interesting pieces of the example solution. The rest of the program's code performs such chores as displaying the counts, fractions, and errors. Download the CPRNG example solution to see additional details.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.203.43