Вариант генератора псевдослучайных чисел

Стандартный генератор

Когда работал над статьёй Подпись данных алгоритмами SHA + AES собственным модулем, обнаружил, что механизм генерации случайных чисел, предоставляемый платформой, мне не совсем подходит. Я использовал его для дополнения кодируемого сообщения.

Объект ГенераторСлучайныхЧислел, предоставляемый платформой, является генератором псевдослучайных чисел. К которым в свою очередь предъявляются определённые требования:

  • достаточно длинный период;
  • эффективность;
  • воспроизводимость;
  • портируемость.
    Обратите внимание на третье требование — воспроизводимость. Это возможность заново воспроизвести ранее сгенерированную последовательность чисел любое количество раз.

ГенераторСлучайныхЧислел полностью соответствует перечисленным требованиям и может быть инициализирован:

  • или другим случайным числом;
  • или текущим временем.
    Приведём пример. Наберите и выполните у себя вот этот код:

    ГСЧ = Новый ГенераторСлучайныхЧисел(1);
    Сообщить(ГСЧ.СлучайноеЧисло());
    Сообщить(ГСЧ.СлучайноеЧисло());
    Сообщить(ГСЧ.СлучайноеЧисло());

    Получилось так?

    1 791 095 845
    4 282 876 139
    3 093 770 124

У меня на разных машинах с разными версиями платформы (пробовал две: 8.3.13.1926 и 8.3.16.1502) всегда получается этот результат.

С другой стороны, чтобы использовать "подсаливание", требуется напротив наиболее случайное число или данные. Воспроизводимость в данном случае — это скорее недостаток. То есть значение соли должно обладать хорошим уровнем энтропии и длиной.

Таким образом, для этих целей, мы не можем использовать инициализацию типового генератора случайных чисел числом, так как для этого нужно другое случайное число. Если оно у нас будет — зачем нам генератор случайных чисел платформы?

Мы так же не можем использовать инициализацию текущим временем (без указания какого-либо числа): раз каждое число инициализации даёт одну и ту же цепочку случайных чисел и поскольку обычно используется одна или несколько первых итераций, то злоумышленнику достаточно знать примерное время генерации случайного числа. Через это можно получить некоторое относительно небольшое количество вариантов для перебора.

Либо "прогонять" в цикле значения N раз, что нерационально.

Кроме всего, на выходе мы получаем 32-битное число. Оно короткое. Хорошо бы 64, а лучше 128.

Какая альтернатива?

Следующим хорошим кандидатом на случайные числа является УникальныйИдентификатор. Здесь очень хорошо описано почему. Вкратце — существует несколько типов уникальных идентификаторов, отличающихся по способу генерации. Если создавать новый уникальный идентификатор (не получая из объекта), то он создаётся всегда с типом 4 — random. При этом используется системный генератор, в котором в качестве случайных параметров используются уникальные параметры системы. То есть повторить цепочку можно только на том же устройстве.

Тип указан всегда в одном и том же месте и у таких УИДов это единственный неслучайный участок: f739997b-6a99-4dd4-a263-7844887f5d47.

Но мы же не хотим, чтобы злоумышленник знал, что у нас там всегда четвёрка? Придётся её затереть, плюс заодно уберём тире:

Функция СлучайныеДанные128бит() Экспорт

    УИД = Новый УникальныйИдентификатор; // Если получать так - то всегда тип 4
    х   = Строка(УИД);

    // Контроль
    Если Сред(х, 15, 1) <> "4"  Тогда
        ВызватьИсключение "Что-то не то! Тип УИДа не равен 4 (random): " + х;
    КонецЕсли;

    // Убираем тире и затираем четвёрку первым случайным символом
    х = Сред(х, 1, 8) + Сред(х, 10, 4) + Сред(х, 1, 1) + Сред(х, 16, 3) + Сред(х, 20, 4) + Сред(х, 25, 12);

    Возврат ПолучитьДвоичныеДанныеИзHexСтроки(х);
КонецФункции // СлучайныеДанные128бит

Обнаружил, что тире можно не убирать. То есть вот так тоже будет работать, но останется четвёрка:

УИД = Новый УникальныйИдентификатор;
СлучайныеДанные128 = ПолучитьДвоичныеДанныеИзHexСтроки(Строка(УИД));

Ещё один вариант с результатом в буфер данных:

Функция СлучайныеБайтыВБуфере128бит() Экспорт

    УИД = Новый УникальныйИдентификатор; // Если получать так - то всегда тип 4
    х   = Строка(УИД);

    // Контроль
    Если Сред(х, 15, 1) <> "4"  Тогда
        ВызватьИсключение "Что-то не то! Тип УИДа не равен 4 (random): " + х;
    КонецЕсли;

    // Убираем тире и затираем четвёрку первым случайным символом
    х = Сред(х, 1, 8) + Сред(х, 10, 4) + Сред(х, 1, 1) + Сред(х, 16, 3) + Сред(х, 20, 4) + Сред(х, 25, 12);

    Возврат ПолучитьБуферДвоичныхДанныхИзHexСтроки(х);  
КонецФункции // СлучайныеБайтыВБуфере128бит

Давайте посчитаем график распределения значений в байтах. Для этого используем такой код:

    РаспределениеЗначений = Новый Массив(256);

    Для НомерИтерации = 1 по 1600 Цикл
        Буфер = СлучайныеБайтыВБуфере128бит();
        Для НомерБайта = 0 по 15 Цикл
            Значение = Буфер[НомерБайта];
            РаспределениеЗначений[Значение] = ?(РаспределениеЗначений[Значение] = Неопределено
                ,1
                ,РаспределениеЗначений[Значение] + 1
            );
        КонецЦикла;
    КонецЦикла;

    Для каждого КоличествоРаз из РаспределениеЗначений Цикл
        Сообщить(Формат(КоличествоРаз, "ЧН=; ЧГ="));
    КонецЦикла;

В результате получаем вот такой график:

Первый ряд — это без замены 4-ки. Второй ряд — с заменой. Оба ряда имеют какую-то "шишку". Очевидно, есть ещё неслучайные биты.

Открываем стандарт RFC 4122 и смотрим структуру GUID:

typedef struct {
    unsigned32  time_low;
    unsigned16  time_mid;
    unsigned16  time_hi_and_version;
    unsigned8   clock_seq_hi_and_reserved;
    unsigned8   clock_seq_low;
    byte        node[6];
} uuid_t;

В параграфе "4.4. Algorithms for Creating a UUID from Truly Random or Pseudo-Random Numbers" также сказано:

The algorithm is as follows:

   o  Set the two most significant bits (bits 6 and 7) of the
      clock_seq_hi_and_reserved to zero and one, respectively.

   o  Set the four most significant bits (bits 12 through 15) of the
      time_hi_and_version field to the 4-bit version number from
      Section 4.1.3.

   o  Set all the other bits to randomly (or pseudo-randomly) chosen
      values.

То есть для приведённого выше примера частично случайным является ещё вот эта часть:

f739997b-6a99-4dd4-a263-7844887f5d47. 

Если заменить это на последний символ, то диаграмма распределения приходит в норму:

Оптимизация

Оптимизировать не удалось. Уникальный идентификатор приводится только к типу "Строка". Нельзя его привести к типу "Число" или записать как-то сразу в бинарные данные. Поэтому пока остаётся неоптимальное преобразование сначала в строку, а потом из строки.

Другие реализации

Есть другие альтернативные реализации случайных данных, можно посмотреть как сделано там: раздва (здесь довольно много вариантов), три, четыре.

Итого

#Область ПРОГРАММНЫЙ_ИНТЕРФЕЙС

Функция СлучайныеБайтыВБуфере128бит() Экспорт

    УИД = Новый УникальныйИдентификатор; // Если получать так - то всегда тип 4
    х   = Строка(УИД);

    Если Сред(х, 15, 1) <> "4"  Тогда
        ВызватьИсключение "Что-то не то! Тип УИДа не равен 4 (random): " + х;
    КонецЕсли;

    х = Сред(х, 1, 8) + Сред(х, 10, 4) + Сред(х, 1, 1) + Сред(х, 16, 3) + Сред(х, 36, 1) + Сред(х, 21, 3) + Сред(х, 25, 12);

    Возврат ПолучитьБуферДвоичныхДанныхИзHexСтроки(х);  
КонецФункции // СлучайныеБайтыВБуфере128бит

Функция СлучайныеДанные128бит() Экспорт

    Буфер   = СлучайныеБайтыВБуфере128бит();
    Поток   = Новый ПотокВПамяти(Буфер);

    Возврат Поток.ЗакрытьИПолучитьДвоичныеДанные();
КонецФункции // СлучайныеДанные128бит

Функция СлучайноеЧисло16бит() Экспорт
    Буфер = СлучайныеБайтыВБуфере128бит();
    Возврат Буфер.ПрочитатьЦелое16();
КонецФункции // СлучайноеЧисло16бит

Функция СлучайноеЧисло32бит() Экспорт
    Буфер = СлучайныеБайтыВБуфере128бит();
    Возврат Буфер.ПрочитатьЦелое32();
КонецФункции // СлучайноеЧисло32бит

Функция СлучайноеЧисло64бит() Экспорт
    Буфер = СлучайныеБайтыВБуфере128бит();
    Возврат Буфер.ПрочитатьЦелое64();
КонецФункции // СлучайноеЧисло64бит

#КонецОбласти // ПРОГРАММНЫЙ_ИНТЕРФЕЙС

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *