使用RNGCryptoServiceProvider生成随机字符串

时间:2015-10-04 10:53:09

标签: c# random

我正在使用此代码生成给定长度的随机字符串

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();
}

但是,我读到RNGCryptoServiceProviderRandom类更安全。如何为此函数实现RNGCryptoServiceProvider。它应该像这个函数一样使用valid字符串。

8 个答案:

答案 0 :(得分:22)

由于RNGRandomNumberGenerator只返回字节数组,你必须这样做:

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();
}

但请注意,这有一个缺陷,62个有效字符等于5,9541963103868752088061235991756位(log(62)/ log(2)),因此它不会在32位数字(uint)上均匀分配。 / p>

这有什么后果? 结果,随机输出将不均匀。有效率较低的字符更有可能发生(只是一小部分,但仍然会发生)。

更准确地说,有效数组的前4个字符更有可能发生0,00000144354999199840239435286%。

为了避免这种情况,你应该使用像64那样均匀划分的数组长度(考虑在输出上使用Convert.ToBase64String,因为你可以干净地匹配64位到6个字节。

答案 1 :(得分:5)

您需要使用byte生成随机RNGCryptoServiceProvider,并仅将有效的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; }

byte

您也可以使用模数来跳过无效的"a" cmp $toto值,但每个字符的可能性都不均匀。

答案 2 :(得分:2)

RNGCryptoServiceProvider以字节的形式返回随机数,因此您需要一种方法从中获取更方便的随机数:

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;
}

然后你可以在你的方法中使用它:

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();
}

(我将该方法设为静态,因为它不使用任何实例数据。)

答案 3 :(得分:2)

请参阅https://bitbucket.org/merarischroeder/number-range-with-no-bias/

我确信我之前已经通过安全实施,没有偏见和良好的性能来回答这个问题。如果是,请发表评论。

看看Tamir的答案,我认为使用模数运算会更好,但会修剪字节值的不完整余数。我现在也在写这个答案(可能再次),因为我需要将这个解决方案引用给同行。

方法1

  • 支持不大于0-255的范围。但它可以回到接近2(这有点慢)
  • 每个值始终使用一个字节。
  • 截断不完整的余数if (buffer[i] >= exclusiveLimit)
  • 调整所需的范围大小。在截断超出exclusiveLimit后,模数保持完美平衡
  • (使用位掩码代替模数是一种较慢的方法)
  • EG。如果你想要一个范围0-16(这是17个不同的值),那么17可以适合15个字节。必须丢弃1个值[255],否则模数将是正常的。

方法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;
                }
            }
        }
    }

方法2

  • 技术上范围从0到ulong.Max可以支持
  • 将RNGCryptoServiceProvider字节视为比特流
  • 计算每个数字所需的base2位长度
  • 从随机比特流中取下一个数字
  • 如果该数字仍然大于所需范围,则丢弃

结果:

  • 请参阅存储库以获取测试工具的最新结果
  • 两种方法似乎都有适当均衡的数字分布
  • 方法1更快[859ms],但它仅适用于单个字节。
  • 方法2比方法1略慢[3038ms],但它跨越字节边界。它丢弃较少的比特,如果随机流输入成为瓶颈(例如,不同的算法),这可能很有用。
  • 这两种方法的混合可以实现两全其美:当字节范围为0-255时速度更快,支持范围超过255但速度稍慢。

答案 4 :(得分:1)

注意:我知道OP的用例偏差,但我认为它可能会帮助那些用例略有不同的人

关于将加密字节数组转换为字符串的原因可以说很多,但通常是出于某种序列化目的;因此,在这种情况下:所选字符集是任意的。

所以,如果且仅当前者为真并且不需要优化字符串的长度;您可以使用字节数组的简单十六进制表示形式,如下所示:

tag_id[]

现在你要说:但是等等:这些只有16个字符,其中OP的序列有62个。你的字符串将会更长。

“是”,我会回答,“如果这是一个问题,为什么不选择256个易于阅读和可串联的字符......或者也许64“; - )

正如@Guffa所述;除非不改变分布,否则禁止使用//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" 。为了实现这一点,给定均匀分布的集合,子集必须在原始集合中完全符合 x 次。

因此,将初始有效集扩展为2会得到有效结果(因为:256/64 = 4)。

代码如下:

%

请注意:在所有答案中,包括这一个,子集小于字节的256种可能性。这意味着可用的信息较少。这意味着如果您的字符串包含4个字符,则更容易破解RNGCryptoServiceProvider的原始4字节结果。

所以...现在你说:“为什么不使用64基本编码?”,好吧,如果它适合你,但要小心尾随//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]); ,请参阅{{ 3}}:

=

请注意²:典型的用例是一些网址令牌。请注意,var rnd = new RNGCryptoServiceProvider(); var buf = new byte[60]; //length not randomly picked, see 64Base, wikipedia rnd.GetBytes(buf); string result = Convert.ToBase64String(buf); 符号无效为此类字符。

答案 5 :(得分:1)

我个人喜欢使用此

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()
        );
    }
}

不应有任何部分需要额外的描述,但为澄清起见,此函数返回随机长度为32到64个字符的随机字符串,并且不使用%(mod),因此应保留均匀性好一点。

我使用它在程序安装时创建一个随机盐,然后将其保存到文件中。因此,在程序运行时,生成的字符串的安全性不是特别需要考虑的问题,因为以后无论如何它都会被写入未加密的文件中。

但是,在更严重的情况下,不应将其原样使用,并且如果要将此值保留在内存中,则应转换为使用SecureString类。在此处阅读更多信息:

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

但是,即使这仅适用于NetFramework,对于NetCore,您还需要找到另一种方法来保护内存中的值。在此处阅读更多信息:

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

答案 6 :(得分:1)

我的实现使用5,9541963103868752082081235991756位修复了该问题

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();
}

答案 7 :(得分:0)

 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();
    }