RNGCryptoServiceProvider-无论char的长度如何,如何获得完全无偏的结果?

时间:2019-06-03 22:36:02

标签: c#

以下方法使用RNGCryptoServiceProvider返回一串随机字符。返回字符串result是通过应用chars从字符串% chars.length中挑选字符而构建的  GetBytes()返回的字节数组中的字节值(0-255)。这意味着根据chars的长度,某些字符可能比其他字符更受青睐。

如何重写该方法,以使chars中的所有字符都具有相等的被选中机会?

/// <summary>
/// Returns a string of cryptographically sound random characters
/// </summary>
/// <param name="type">Accepted parameter variables are HEX (0-F), hex (0-f),
/// DEC/dec/NUM/num (0-9), ALPHA (A-Z), alpha (a-z), ALPHANUM (A-Z and 0-9),
/// alphanum (a-z and 0-9) and FULL/full (A-Z, a-z and 0-9)</param>
/// <param name="length">The length of the output string</param>
/// <returns>String of cryptographically sound random characters</returns>
private static string Serial(string type, int length)
{
    if (length < 1) return "";
    string chars;
    switch (type)
    {
        case "HEX":
            chars = "0123456789ABCDEF";
            break;
        case "hex":
            chars = "0123456789abcdef";
            break;
        case "DEC":
        case "dec":
        case "NUM":
        case "num":
            chars = "0123456789";
            break;
        case "ALPHA":
            chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            break;
        case "alpha":
            chars = "abcdefghijklmnopqrstuvwxyz";
            break;
        case "ALPHANUM":
            chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            break;
        case "alphanum":
            chars = "abcdefghijklmnopqrstuvwxyz0123456789";
            break;
        case "FULL":
        case "full":
            chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            break;
        default:
            chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            break;
    }
    byte[] data = new byte[length];
    using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
    {
        crypto.GetBytes(data);
    }
    StringBuilder result = new StringBuilder(length);
    foreach (byte b in data)
    {
        result.Append(chars[b % chars.Length]);
    }
    return result.ToString();
}

1 个答案:

答案 0 :(得分:2)

Rejection sampling似乎是最实用,最公正的方法,

private const int blockSize = 1024;
private readonly RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();

private static IEnumerable<byte> GetCryptoStream()
{
    var data = new byte[blockSize];
    while (true)
    {
        crypto.GetBytes(data);
        foreach (var b in data) yield return b;
    }
}

private static IEnumerable<int> GetCryptoStream(int radix)
{
    if (radix <= 0 || radix >= 256) throw new ArgumentException("radix should be > 0 and < 256");

    var rem = 256 % radix;
    foreach (var b in GetCryptoStream())
    {
        if (b < rem) continue;      // rejection sampling
        yield return b % radix;
    }
}

private static string Serial(string type, int length)
{
    if (length < 1) return "";
    string chars;
    switch (type)
    {
        // ...
    }

    var result = new StringBuilder(length);
    foreach (var k in GetCryptoStream(chars.Length).Take(length))
    {
        result.Append(chars[k]);
    }

    return result.ToString();
}

测试代码,

[TestCase(3)]
[TestCase(6)]
[TestCase(5)]
[TestCase(75)]
public void TestCrypto(int n)
{
    const int m = 1000 * 1000 * 100;
    var count = new int[n];
    foreach (var k in GetCryptoStream(n).Take(m))
    {
        count[k] ++;
    }

    Console.WriteLine($"Expected : {1.0 / n:F4}, Actual :");
    for (int i = 0; i < count.Length; i++)
    {
        Console.WriteLine($"{i} : {count[i] / (double) m:F4}");
    }
}

结果为6

Expected : 0.1667, Actual :
0 : 0.1667
1 : 0.1667
2 : 0.1667
3 : 0.1667
4 : 0.1667
5 : 0.1666