Стандартный генератор
Когда работал над статьёй Подпись данных алгоритмами 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-4
dd4-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-a
263-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бит
#КонецОбласти // ПРОГРАММНЫЙ_ИНТЕРФЕЙС