使用RNGCryptoServiceProvider生成随机字符串

meh*_*met 19 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();
}
Run Code Online (Sandbox Code Playgroud)

但是,我读到这RNGCryptoServiceProviderRandom课堂更安全.我RNGCryptoServiceProvider该如何实现这个功能.它应该valid像这个函数一样使用字符串.

hl3*_*kel 27

由于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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 将字节值转换为特定范围内的数字时必须小心.如果你只使用`%`那么它将产生分布不均匀的数字,并且使用更好的随机生成器的整个目的是没有意义的. (5认同)
  • 这是真的,想在我发布我的答案后添加它.Tamir Vered的答案非常有趣,虽然因为它不使用StringBuilder而效率非常低.它省去了不被接受作为输入的字节,这是一个有趣的解决方案,并且将具有适当的分布 (2认同)

Tam*_*red 7

您需要生成随机bytes 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;
}
Run Code Online (Sandbox Code Playgroud)

您也可以使用modulo以便不跳过无效byte值,但每个字符的机会不均匀.


Guf*_*ffa 5

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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();
}
Run Code Online (Sandbox Code Playgroud)

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

  • 可能值得解释一下 while 循环中的条件以及它如何保证均匀分布 mod `max`。 (2认同)

Ste*_*fan 5

笔记

我知道 OP 用例的偏差,但我认为这可能会帮助那些没有“任意”62 个字符限制并且只想使用 RNGCryptoServiceProvider 对字节进行编码以生成随机字符串的其他人。

TL; 博士

直接跳到底部,base64 编码的情况。


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

所以,如果是关于序列化,你有很多选择;例如:

  • 文本作为十六进制表示
  • 文本作为 base64 表示
  • 文本作为替代表示

所有这些都在利用相同的东西:以适合在不支持本地二进制传输的介质中传输的方式对数字进行编码。

我称其为“作为...表示的文本”,因为最终将传输的是文本。

十六进制示例:

//note: using text as HEX makes the result longer
var crypt = new RNGCryptoServiceProvider();
var sb = new StringBuilder();
var buf = new byte[10]; //length: should be larger
crypt.GetBytes(buf);

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

//sb contains a RNGCryptoServiceProvider based "string"
Run Code Online (Sandbox Code Playgroud)

现在你会说:

但是等等:这些只有 16 个字符,其中 OP 的序列有 62。62 比 16 更有效率,因此,转换为文本后,您的字符串会更长。

“是的”,我会说,“如果这是一个问题,为什么不选择更多的易于阅读和可序列化的字符……62……或者 64”

代码将是:

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

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

foreach (byte b in buf)
    sb.Append(valid[b%64]);
Run Code Online (Sandbox Code Playgroud)

注意:正如@Guffa 所说;%除非不改变分布,否则禁止使用。为了实现这一点,给定一个均匀分布的集合,该子集必须恰好适合原始集合的x次。

因此,用 2 扩展初始有效集会给出有效结果(因为:256 / 64 = 4)---但是,这不符合 OP 的 62 个字符要求。事实上,为了获得均匀的分布,你需要一些技巧,在其他答案中解决。

另请注意:在所有答案中,包括这个答案,子集都小于字节的 256 种可能性。这意味着编码字符中可用的信息少于字节中的可用信息。这意味着如果您的字符串包含 4 个编码字符,则更容易破解 RNGCryptoServiceProvider 的原始 4 字节结果 - 所以请记住,加密强度取决于字节长度,而不是编码字符长度。

Base64

但是,现在你说:

“好吧,放弃 62 的要求,使用 64 - 为什么不使用 64 基本编码?” ,

好吧,如果它适合您,但请注意尾随=,请参阅维基百科上的 Base64,它是使用的附加可选字符。

var crypt = new RNGCryptoServiceProvider();
// = padding characters might be added to make the last encoded block 
// contain four Base64 characters. 
// which is actually an additional character
var buf = new byte[10]; 
crypt.GetBytes(buf);

//contains a RNGCryptoServiceProvider random string, which is fairly readable
//and contains max 65 different characters.
//you can limit this to 64, by specifying a different array length.
//because log2(64) = 6, and 24 = 4 x 6 = 3 x 8
//all multiple of 3 bytes are a perfect fit.  (e.g.: 3, 6, 15, 30, 60)
string result = Convert.ToBase64String(buf);
Run Code Online (Sandbox Code Playgroud)


Mic*_*uba 5

我的实现用 5,9541963103868752088061235991756 位解决了这个问题

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();
}
Run Code Online (Sandbox Code Playgroud)