Используя 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();
    }