Используя RNGCryptoServiceProvider для генерации случайной строки
Я использую этот код для генерации случайных строк с заданной длиной
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();
}
Однако я читал, что RNGCryptoServiceProvider
более безопасен, чем класс Random
. Как я могу реализовать RNGCryptoServiceProvider
для этой функции. Он должен использовать строку valid
точно так же, как эта функция.
Ответы
Ответ 1
Поскольку 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).
Какие последствия это имеет? В результате случайный вывод не будет равномерным. Символы, которые имеют более низкое значение, будут встречаться с большей вероятностью (лишь небольшая доля, но все же это происходит).
Чтобы быть более точным, первые 4 символа допустимого массива с вероятностью 0,00000144354999199840239435286% встречаются чаще.
Чтобы избежать этого, вы должны использовать длины массивов, которые будут делиться равномерно на 64 (вместо этого рассмотрите возможность использования Convert.ToBase64String в выходных данных, поскольку вы можете безошибочно сопоставить 64 бита с 6 байтами.
Ответ 2
Вам нужно сгенерировать случайный 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;
}
Вы также можете использовать modulo, чтобы не пропускать недопустимые значения byte
, но шансы для каждого символа не будут четными.
Ответ 3
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();
}
(я сделал метод статическим, так как он не использует данные экземпляра.)
Ответ 4
см. https://bitbucket.org/merarischroeder/number-range-with-no-bias/
Я уверен, что уже отвечал на этот вопрос с безопасной реализацией, без предвзятости и хорошей производительности. Если это так, пожалуйста, прокомментируйте.
Глядя на ответ Тамира, я подумал, что было бы лучше использовать операцию модуля, но обрезать неполный остаток байтовых значений. Я также пишу этот ответ сейчас (возможно, снова), потому что мне нужно отослать это решение к коллеге.
Подход 1
- Поддержка диапазонов не больше 0-255. Но он может вернуться к подходу 2 (который немного медленнее)
- Один байт всегда используется для каждого значения.
- Обрезать неполный остаток
if (buffer[i] >= exclusiveLimit)
- Модулируйте желаемый размер диапазона. После усечения за пределами исключительного ограничения модуль остается идеально сбалансированным
- (Использование битовой маски вместо модуля - более медленный подход)
- НАПРИМЕР. Если вы хотите диапазон 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. Макс может поддерживаться
- Обрабатывать байты RNGCryptoServiceProvider как поток битов
- Рассчитайте длину в битах base2, необходимую для каждого числа
- Возьмите следующий номер из случайного потока битов
- Если это число все еще превышает желаемый диапазон, откажитесь
Результаты:
- См. Репозиторий для последних результатов из тестового жгута
- Оба подхода имеют подходящее сбалансированное распределение чисел
- Подход 1 быстрее [859 мс], но он работает только с отдельными байтами.
- Подход 2 немного медленнее [3038 мс], чем подход 1, но он работает через границы байтов. Он отбрасывает меньше битов, что может быть полезно, если случайный входной поток становится узким местом (например, другой алгоритм).
- Гибрид обоих подходов дает лучшее из обоих миров: лучшую скорость, когда диапазон байтов 0-255, поддержка диапазонов за пределами 255, но немного медленнее.
Ответ 5
Примечание: я знаю об отклонениях в использовании прецедентов, но я думаю, что это может помочь другим, у которых есть немного другой случай использования
Можно многое сказать о причинах преобразования массива криптографических байтов в строку, но обычно это для каких-то сериализации; и, следовательно, в этом случае: выбранный набор символов является произвольным.
Итак, если и только если первое верно И длина строки не требуется для оптимизации; вы можете использовать простое шестнадцатеричное представление массива байтов следующим образом:
//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"
Теперь вы скажете: но подождите: это всего 16 символов, где OP-последовательность имеет 62. Ваша строка будет намного длиннее.
"Да", я отвечу ", и если это проблема, почему бы вам не выбрать 256 простых для чтения и сериализуемых символов... или, возможно, 64"; -)
Как сказал @Гуффа; использование %
запрещено, если оно не изменяет распределение. Чтобы это произошло, с учетом равномерно распределенного набора подмножество должно соответствовать ровно х раз в исходном наборе.
Итак, расширение вашего исходного действительного набора с помощью 2 дает правильный результат (потому что: 256/64 = 4).
Код:
//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]);
Обратите внимание: во всех ответах, включая этот, подмножество меньше 256 возможных байтов. Это означает, что имеется меньше информации. Это означает, что если у вас есть строка с 4 символами, проще взломать исходный 4-байтовый результат RNGCryptoServiceProvider.
Итак... теперь вы говорите: "Почему бы не использовать 64-битную кодировку?", ну, если вам это подходит, но будьте осторожны с конечным =
, см. Base64 в Википедии:
var rnd = new RNGCryptoServiceProvider();
var buf = new byte[60]; //length not randomly picked, see 64Base, wikipedia
rnd.GetBytes(buf);
string result = Convert.ToBase64String(buf);
Обратите внимание, что 2: Типичным вариантом использования является некоторый токен URL. Обратите внимание, что знак +
недействителен как такой символ.
Ответ 6
Мне лично нравится использовать это:
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
Ответ 7
Моя реализация, которая исправляет проблему с 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();
}
Ответ 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();
}