RNGCryptoServiceProvider GetBytes() - как ограничить возвращаемые значения?
У меня есть этот метод, который возвращает строку криптографически сильных случайных символов.
Первый, GetBytes()
заполняет байтовый массив значениями от 0 до 255.
Далее, возвращаемая строка строится путем выбора номера символа {byte value} % {length of character set}
из набора символов.
Проблема в том, что длина большинства моих наборов символов не делится поровну на 256, поэтому результат будет смещен в сторону некоторых символов. Если, например, набор символов имеет длину 8, 16 или 32 символа, остаток равен 0, и проблем нет.
Вот я и подумал - можно ли ограничить значения, возвращаемые GetBytes()
таким образом, что длина набора символов будет равномерно делиться на максимальное значение? Например, если набор символов имеет длину 62, максимальное значение должно быть 247.
Конечно, я мог бы получить только один байт за раз, и если значение было слишком высоким, я мог бы получить еще один. Но это не очень элегантно.
/// <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>
public static string Serial(string type, int length)
{
if (length < 1) return "";
string chars;
switch (type)
{
case "HEX":
chars = "0123456789ABCDEF"; // 16
break;
case "hex":
chars = "0123456789abcdef"; // 16
break;
case "DEC":
case "dec":
case "NUM":
case "num":
chars = "0123456789"; // 10
break;
case "ALPHA":
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 26
break;
case "alpha":
chars = "abcdefghijklmnopqrstuvwxyz"; // 26
break;
case "ALPHANUM":
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // 36
break;
case "alphanum":
chars = "abcdefghijklmnopqrstuvwxyz0123456789"; // 36
break;
case "FULL":
case "full":
default:
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // 62
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 ответ
Как указано в комментариях, выборка отклонения является стандартным способом сделать это. Мы можем амортизировать некоторые затраты, переместив наше использование провайдера RNG Crypto в вспомогательный метод, чтобы нам не приходилось работать с ним побайтово-агонизирующим байтом:
public static string Serial(string type, int length)
{
if (length < 1) return "";
string chars;
switch (type)
{
case "HEX":
chars = "0123456789ABCDEF"; // 16
break;
case "hex":
chars = "0123456789abcdef"; // 16
break;
case "DEC":
case "dec":
case "NUM":
case "num":
chars = "0123456789"; // 10
break;
case "ALPHA":
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 26
break;
case "alpha":
chars = "abcdefghijklmnopqrstuvwxyz"; // 26
break;
case "ALPHANUM":
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // 36
break;
case "alphanum":
chars = "abcdefghijklmnopqrstuvwxyz0123456789"; // 36
break;
case "FULL":
case "full":
default:
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // 62
break;
}
int limit = (256 / chars.Length) * chars.Length;
StringBuilder result = new StringBuilder(length);
foreach (byte b in SecureBytesInRange(limit,length))
{
result.Append(chars[b % chars.Length]);
}
return result.ToString();
}
private const int SECURE_BYTE_BUFFER_SIZE = 32;
static IEnumerable<byte> SecureBytesInRange(int exclusiveUpperBound, int countRequired)
{
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
{
byte[] buffer = new byte[SECURE_BYTE_BUFFER_SIZE];
int ix = SECURE_BYTE_BUFFER_SIZE;
int countProduced = 0;
while (countProduced < countRequired)
{
if (ix == SECURE_BYTE_BUFFER_SIZE)
{
crypto.GetBytes(buffer);
ix = 0;
}
while (ix < SECURE_BYTE_BUFFER_SIZE)
{
if (buffer[ix] < exclusiveUpperBound)
{
yield return buffer[ix];
countProduced++;
if (countProduced == countRequired) break;
}
ix++;
}
}
}
}
Как я также указал в комментариях, я бы создал перечисление для поддерживаемых типов кодирования, а не с использованием строки, или, в качестве альтернативы, с именованными константами / свойствами, которые возвращают фактические диапазоны символов, которые будут использоваться, так что вместо этого вы ' ре прямо передано chars
скорее, чем type
(это также добавляет гибкости, позволяя использовать вашу функцию с другими диапазонами символов, отличными от тех, которые вы можете себе представить.