Using the .Net RNGCryptoServiceProvider in Powershell

I was working on a password generator function, and I wanted to use something that was a better random number generator than Powershell’s Get-Random cmdlet. I’m not a crypto expert, so I’ll leave the discussions about how good each random number generator is, but Matt Graeber wrote a nice article that discusses how to test the effectiveness of a random number generator. According to Matt’s testing, the RNGCryptoServiceProvider class is a better random number generator that Powershell’s Get-Random for cryptographic purposes. I liked the way Get-Random worked, so I decided to implement a function that worked in a similar manner. Note, Get-Random works two ways: it either returns a random number or returns a random object from a collection of objects. This implementation only returns a random number.

RNGCryptoServiceProvider implements a GetType method that is passed an array of bytes and returns it filled with random byte values. The trick here is, that I want to return a single unsigned 32 bit integer but GetType returns an array of bytes. I need to use the System.BitConverter .NET class to convert an array of bytes to a single uint32 value. The other challenge I ran into is how to implement minimum and maximum values. I decided to use the mod operator to return a value in the range I need.

Here is the function I came up with:

Function Get-RandomInt {
    [cmdletbinding()]
    Param(
        [uint32]$max=[UInt32]::MaxValue,
        [uint32]$min=[UInt32]::MinValue
    )
    if ($min -lt $max) {
        #initialize everything
        $diff=$max-$min+1
        [Byte[]] $bytes = 1..4  #4 byte array for int32/uint32
        $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
        #generate the number
        $rng.getbytes($bytes)
        $number = [System.BitConverter]::ToUInt32(($bytes),0)
        $number = $number % $diff + $min
        return $number    
    } else {
        Write-Warning 'Min must be less than Max'
        return -1
    }
}

Use it like this:

Get-RandomInt -min 1 -max 100

to get a random number between 1 and 100.

 

…Tim

6 thoughts on “Using the .Net RNGCryptoServiceProvider in Powershell

  1. Pingback: Generating Diceware Passphrases with Powershell | Tim's Blog

  2. Tibo

    Hi,

    I think you need to add 1 to your max variable before you calculate the $diff variable else you wont get your max number.

    Kind Regards,

    Tibo

    Reply
  3. NovHak

    Hi Tim,

    You implementation has a bias, which is particularly worth noticing since you seem to have security concerns.

    The problem occurs if [UInt32]::MaxValue is not a multiple of $diff, i.e. $diff is not a power of 2, which is by far the most likely. Otherwise it’s OK, any bias could only come from the RNGCryptoServiceProvider class itself or especially vicious initial parameters.

    For the sake of simplicity, let’s take the case when the max value is 7 (hence the RNGCryptoServiceProvider method gives one value between 0 and 7), and you want a number between 0 and 6, hence $diff -eq 7.

    0 is twice more likely to be drawn than the other numbers, because depending of what the RNGCryptoServiceProvider RNG gives : 0 % 7 = 0, 1 % 7 = 1, 2 % 7 = 2, …, 6 % 7 = 6, 7 % 7 = 0.

    Hence, two RNG drawings (0 and 7) can yield to 0, instead of one for the others. Hence the bias.

    More generally, if you want to extract a value in the interval [0,V-1] from a drawing in [0,N-1] with the remainder method, the values in the interval [0,(N%V)-1] will have a higher probability to be drawn than the ones in [N%V,V-1].

    The only solution I see to this problem is to define a “bad zone” where you have to redraw if RNGCryptoServiceProvider falls into it. One way to do this (in our binary case where N is a power of 2) is defining a bit mask 2^M-1, being the smallest possible such as 2^M-1>=V-1 (M is an integer of course). Bitwise-and the drawing with the mask, which will give a result in [0,2^M-1]; if the result doesn’t fall into [0,V-1], trash and redraw.

    Powershelly speaking : $number=$number -band $mask, then if $number -lt $diff it’s OK, otherwise you have to redraw.

    Now theoretically speaking it’s possible that you’ll have to trash a billion drawings before getting a good one, even wait your whole life watching the Powershell prompt, but this bitmask method guarantees a more than 50% probability that each drawing is OK, so that would be a big piece of bad luck :D

    Most people think once they got the number from the RNG it’s OK but often it’s not. Powershell’s Get-Random hides this since you can specify directly in which interval you want the result, which raises a concern btw : did MS make the same mistake you did ? I can answer this since I saw the code of Gen-Random, and it’s far worse than that (see bottom note).

    Finally, I found your web page because I’m not quite fluent in Powershell and wanted to make the same thing as you. Now that you helped me find a starting point, I’m going to make my own function and post it back here once it’s done, so see you soon !

    NOTE :
    Concerning the Get-Random code, first of all you can have a look at it by yourself, as this article explains : https://blogs.msdn.microsoft.com/luisdem/2016/05/19/get-the-source-code-of-the-powershell-cmdlets/

    Now, if I understand their code correctly : Get-Random seems to use the same RNGCryptoServiceProvider unless a seed is provided (in which case another non-cryptographic generator is used), they get a four byte array with GetBytes(), convert it to int32 (not uint32), but (and here’s Johnny !) they convert the result to a floating point value between 0 and 1 which they multiply by $diff and then add $min. This conversion to floating point is just catastrophic, it completely ruins integer precision ! They do this by multiplying by either 4.6566128752457969E-10 (which is an approximation of 1/[int32]::MaxValue) or 2.3283064376228985E-10 (which approximates 1/[uint32]::MaxValue-2, likely for dirty floating point issues they must have encountered) depending on the value of $diff being over [int32]::MaxValue or not.

    Reply
  4. NovHak

    Hi Tim, I’m back with my solution.

    I don’t input ranges in my function, only the number of faces of the dice. I’m minimizing what I ask from the RNG, which you may find rather useless ! The face number is returned, zero being the first one. Hope this helps.

    function NH-Get-SecRandom ([uint64]$nb) {
    if ($nb -eq 0) { return $null }
    if ($nb -eq 1) { return 0 }
    –$nb # $nb is no more the number of faces of the dice, it’s the maximum value wanted
    [uint64]$mask=1
    $bits=1
    $breq=1 # Bytes to be requested from the RNG
    # We’re increasing the mask until it covers all of $nb’s significant bits.
    # At the same time, the number of bytes to be requested from the generator is being computed
    while ( ($mask -band $nb) -ne $nb ) {
    $mask=$mask -shl 1
    $mask=$mask -bor 1
    ++$bits
    if (($bits % 8) -eq 1) { ++$breq }
    }
    [byte[]]$draw=1..$breq
    $rng=[System.Security.Cryptography.RNGCryptoServiceProvider]::Create()
    # Drawing, masking the result and redrawing until the result is below or equal to the max value wanted
    do {
    $rng.GetBytes($draw)
    [uint64]$idraw=0
    $draw | foreach { $idraw=($idraw -shl 8) -bor $_ }
    $idraw=$idraw -band $mask
    }
    until ($idraw -le $nb) # Anything in the interval [$nb+1,$mask] has to be discarded
    return $idraw
    }

    Reply
  5. NovHak

    Dear Tim,

    It’s me again ! I just thought my explanation may not have been clear enough, so may I point you to this page containing a C# example from Microsoft on how to make a dice roll with RNGCryptoServiceProvider :

    https://docs.microsoft.com/dotnet/api/system.security.cryptography.rngcryptoserviceprovider?view=netframework-4.7.2

    It’s in C# but I hope it’s readable enough that you will understand the importance of the IsFairRoll() function, which removes that bias I tried to warn you about.

    I personally don’t like using the division operator in what is an integer-only context which is why my solution is different (I don’t want ugly floating point processing behind the scenes), but MS’ one should work too, after all.

    Hope this helps.

    Reply

Leave a Reply to NovHak Cancel reply

Your email address will not be published. Required fields are marked *