using RNGCryptoServiceProvider to generate random

2019-02-01 23:10发布

问题:

I'm using this code to generate random strings with given length

public string RandomString(int length)
{
    const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    StringBuilder res = new StringBuilder();
    Random rnd = new Random();
    while (0 < length--)
    {
        res.Append(valid[rnd.Next(valid.Length)]);
    }
    return res.ToString();
}

However, I read that RNGCryptoServiceProvideris more secure than Random class. How can I implement RNGCryptoServiceProvider to this function. It should use valid string just like this function.

回答1:

Since RNGRandomNumberGenerator only returns byte arrays, you have to do it like this:

static string RandomString(int length)
{
    const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    StringBuilder res = new StringBuilder();
    using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
    {
        byte[] uintBuffer = new byte[sizeof(uint)];

        while (length-- > 0)
        {
            rng.GetBytes(uintBuffer);
            uint num = BitConverter.ToUInt32(uintBuffer, 0);
            res.Append(valid[(int)(num % (uint)valid.Length)]);
        }
    }

    return res.ToString();
}

Note however that this has a flaw, 62 valid characters is equal to 5,9541963103868752088061235991756 bits (log(62) / log(2)), so it won't divide evenly on a 32 bit number (uint).

What consequences does this have? As a result, the random output won't be uniform. Characters which are lower in valid will occur more likely (just by a small fraction, but still it happens).

To be more precise, the first 4 characters of the valid array are 0,00000144354999199840239435286 % more likely to occur.

To avoid this, you should use array lengths which divide evenly like 64 (Consider using Convert.ToBase64String on the output instead, since you can cleanly match 64 bits to 6 bytes.



回答2:

You need to generate random bytes using RNGCryptoServiceProvider and append only the valid ones to the returned string:

const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";

static string GetRandomString(int length)
{
    string s = "";
    using (RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider())
    {
        while (s.Length != length)
        {
            byte[] oneByte = new byte[1];
            provider.GetBytes(oneByte);
            char character = (char)oneByte[0];
            if (valid.Contains(character))
            {
                s += character;
            }
        }
    }
    return s;
}

You could also use modulo in order to not skip the invalid byte values but that the chances for each character won't be even.



回答3:

The RNGCryptoServiceProvider returns random numbers in the form of bytes, so you need a way to get a more convenient random number from it:

public static int GetInt(RNGCryptoServiceProvider rnd, int max) {
  byte[] r = new byte[4];
  int value;
  do {
    rnd.GetBytes(r);
    value = BitConverter.ToInt32(r, 0) & Int32.MaxValue;
  } while (value >= max * (Int32.MaxValue / max));
  return value % max;
}

Then you can use that in your method:

public static string RandomString(int length) {
  const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
  StringBuilder res = new StringBuilder();
  using (RNGCryptoServiceProvider rnd = new RNGCryptoServiceProvider()) {
    while (length-- > 0) {
      res.Append(valid[GetInt(rnd, valid.Length)]);
    }
  }
  return res.ToString();
}

(I made the method static, as it doesn't use any instance data.)



回答4:

see https://bitbucket.org/merarischroeder/number-range-with-no-bias/

I'm sure I have answered this one before with a secure implementation, no bias, and good performance. If so, please comment.

Looking at Tamir's answer, I thought it would be better to use the modulus operation, but trim off the incomplete remainder of byte values. I'm also writing this answer now (possibly again), because I need to reference this solution to a peer.

Approach 1

  • Support for ranges that are no bigger than 0-255. But it can fall back to approach 2 (which is a little slower)
  • One byte is always used per value.
  • Truncate the incomplete remainder if (buffer[i] >= exclusiveLimit)
  • Modulate the desired range size. After truncation beyond the exclusiveLimit the modulus remains perfectly balanced
  • (Using a bitmask instead of modulus is a slower approach)
  • EG. If you want a range 0-16 (that's 17 different values), then 17 can fit into a byte 15 times. There is 1 value that must be discarded [255], otherwise the modulus will be fine.

Code for Approach 1

    const string lookupCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";

    static void TestRandomString()
    {
        Console.WriteLine("A random string of 100 characters:");

        int[] randomCharacterIndexes = new int[100];
        SecureRangeOriginal(randomCharacterIndexes, lookupCharacters.Length);
        var sb = new StringBuilder();
        for (int i = 0; i < randomCharacterIndexes.Length; i++)
        {
            sb.Append(lookupCharacters[randomCharacterIndexes[i]]);
        }
        Console.WriteLine(sb.ToString());

        Console.WriteLine();
    }

    static void SecureRangeOriginal(int[] result, int maxInt)
    {
        if (maxInt > 256)
        {
            //If you copy this code, you can remove this line and replace it with `throw new Exception("outside supported range");`
            SecureRandomIntegerRange(result, 0, result.Length, 0, maxInt);  //See git repo for implementation.
            return;
        }

        var maxMultiples = 256 / maxInt; //Finding the byte number boundary above the provided lookup length - the number of bytes
        var exclusiveLimit = (maxInt * maxMultiples); //Expressing that boundary (number of bytes) as an integer

        var length = result.Length;
        var resultIndex = 0;

        using (var provider = new RNGCryptoServiceProvider())
        {
            var buffer = new byte[length];

            while (true)
            {
                var remaining = length - resultIndex;
                if (remaining == 0)
                    break;

                provider.GetBytes(buffer, 0, remaining);

                for (int i = 0; i < remaining; i++)
                {
                    if (buffer[i] >= exclusiveLimit)
                        continue;

                    var index = buffer[i] % maxInt;
                    result[resultIndex++] = index;
                }
            }
        }
    }

Approach 2

  • Technically ranges from 0 to ulong.Max can be supported
  • Treat RNGCryptoServiceProvider bytes as a bitstream
  • Calculate the base2 bit length needed per number
  • Take the next number from the random bitstream
  • If that number is still greater than the desired range, discard

Results:

  • See the repository for the latest results from the test harness
  • Both approaches appear to have a suitably balanced distribution of numbers
  • Approach 1 is faster [859ms] but it only works on individual bytes.
  • Approach 2 is a little slower [3038ms] than Approach 1, but it works across byte boundaries. It discards fewer bits, which can be useful if the random stream input becomes a bottleneck (different algorithm for example).
  • A hybrid of both approaches gives the best of both worlds: better speed when the byte range is 0-255, support for ranges beyond 255 but a bit slower.


回答5:

note: I am aware of OP's use-case deviation, but I think it might help others who have a slightly different use-case

A lot can be said about the reasons to convert a cryptographic byte array into a string, but usually it is for some sort of serialization purposes; and hence, in that case: the selected character set is arbitrary.

So, if, and only if the former is true AND the length of the string doesn't required to be optimized; you can use a simple hexadecimal representation of the byte array like this:

//note: since the choice of characters [0..9a..zA...Z] is arbitrary, 
//limiting to [0..9,A..F] would seem to be a really big problem   if it can be compensated
//by the length.

var rnd = new RNGCryptoServiceProvider();
var sb = new StringBuilder();
var buf = new byte[10]; //length: should be larger
rnd.GetBytes(buf);

//gives a "valid" range of: "0123456789ABCDEF"   
foreach (byte b in buf)
    sb.AppendFormat("{0:x2}", b);

//sb contains a RNGCryptoServiceProvider based "string"

Now you'll say: but wait: these are only 16 characters where OP's sequence has 62. Your string will be a lot longer.

"Yes", I will answer, "and if that's a problem, why don't you pick 256 easy-to-read-and-serrializable-characters... or perhaps 64" ;-)

As @Guffa stated; using % is forbidden unless it doesn't alter the distribution. To make this happen, given a evenly distributed set, the subset must fit exactly x times in the original set.

So, expanding your initial valid set with 2 gives a valid result (because: 256 / 64 = 4).

The code would be:

//note: added + and / chars. could be any of them
const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890+/";

var rnd = new RNGCryptoServiceProvider();
var sb = new StringBuilder();
var buf = new byte[10]; //length: should be larger
rnd.GetBytes(buf); //get the bytes

foreach (byte b in buf)
    sb.Append(valid[b%64]);

Please note: in all the answers, including this one, the sub-set is smaller than the 256 possibilities of the byte. This means there is less information available. This means if you have your string with 4 chars, it's easier to crack the original 4 byte result of the RNGCryptoServiceProvider.

So... now you say: "Why not use 64 base encoding?", well, if it's suits you, but be careful with the trailing =, see Base64 on Wikipedia:

var rnd = new RNGCryptoServiceProvider();
var buf = new byte[60]; //length not randomly picked, see 64Base, wikipedia
rnd.GetBytes(buf);
string result = Convert.ToBase64String(buf);

Please note²: A typical use case is some url token. Please note that the + sign is invalid as such a character.



回答6:

I personally like to use this:

private static string GenerateRandomSecret()
{
    var validChars = Enumerable.Range('A', 26)
        .Concat(Enumerable.Range('a', 26))
        .Concat(Enumerable.Range('0', 10))
        .Select(i => (char)i)
        .ToArray();

    var randomByte = new byte[64 + 1]; // Max Length + Length

    using (var rnd = new RNGCryptoServiceProvider())
    {
        rnd.GetBytes(randomByte);

        var secretLength = 32 + (int)(32 * (randomByte[0] / (double)byte.MaxValue));

        return new string(
            randomByte
                .Skip(1)
                .Take(secretLength)
                .Select(b => (int) ((validChars.Length - 1) * (b / (double) byte.MaxValue)))
                .Select(i => validChars[i])
                .ToArray()
        );
    }
}

There shouldn't be any part that needs additional description, but to clarify, this function returns a random string with a random length between 32 and 64 chars and doesn't use % (mod) therefore should keep uniformity a little better.

I use this to create a random salt at program installation and later save it to a file. Therefore security of generated string is not of special concern while the program is running as it is going to get written to an unencrypted file later on anyway.

However, for more serious situations, this shouldn't be used as it is and should be converted to use SecureString class if you are going to keep this value in memory. Read more here:

https://docs.microsoft.com/en-us/dotnet/api/system.security.securestring?redirectedfrom=MSDN&view=netframework-4.7.2

However, even this only applies to NetFramework, for NetCore you need to find another way to secure the value in the memory. Read more here:

https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md



回答7:

My implementation that fixes the issue with 5,9541963103868752088061235991756 bits

public static string RandomString(int length)
{
    const string alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    var res = new StringBuilder(length);
    using (var rng = new RNGCryptoServiceProvider())
    {
        int count = (int)Math.Ceiling(Math.Log(alphabet.Length, 2) / 8.0);
        Debug.Assert(count <= sizeof(uint));
        int offset = BitConverter.IsLittleEndian ? 0 : sizeof(uint) - count;
        int max = (int)(Math.Pow(2, count*8) / alphabet.Length) * alphabet.Length;
        byte[] uintBuffer = new byte[sizeof(uint)];

        while (res.Length < length)
        {
            rng.GetBytes(uintBuffer, offset, count);
            uint num = BitConverter.ToUInt32(uintBuffer, 0);
            if (num < max)
            {
                res.Append(alphabet[(int) (num % alphabet.Length)]);
            }
        }
    }

    return res.ToString();
}


回答8:

 private string sifreuretimi(int sayı) //3
    {
        Random rastgele = new Random();  
        StringBuilder sb = new StringBuilder();
        char karakter1 = ' ', karakter2 = ' ', karakter3 = ' ';
        int ascii1, ascii2, ascii3 = 0;

        for (int i = 0; i < sayı/3; i++)
        {
            ascii1 = rastgele.Next(48,58);
            karakter1 = Convert.ToChar(ascii1);

            ascii2 = rastgele.Next(65, 91);
            karakter2 = Convert.ToChar(ascii2);

            ascii3 = rastgele.Next(97, 123);
            karakter3 = Convert.ToChar(ascii3);

            sb.Append(karakter1);
            sb.Append(karakter2);
            sb.Append(karakter3);
        }
        return sb.ToString();
    }


标签: c# random