Получение действительно случайного числа в Arduino

Какой наилучший способ получить действительно (в отличие от псевдо) случайное число в Arduino или, по крайней мере, наилучшее возможное приближение? Насколько я понимаю, функция randomSeed(analogRead(x)) недостаточно случайна.

Если возможно, метод должен использовать только базовую настройку Arduino (без дополнительных датчиков). Решения с внешними датчиками приветствуются, если они значительно улучшают случайность по сравнению с базовой настройкой.

, 👍13

Обсуждение

Что такое приложение? Должен ли он быть криптографически безопасным? Что вы тогда делаете со случайностью? Тогда без внешнего чипа, реализующего TRNG из физического источника энтропии, вам не повезло. Вы также можете реализовать детерминированный RNG, такой как HMAC DRBG, и создать его из чего-то статического плюс низкокачественный источник энтропии, но это все равно не будет криптографически безопасным., @Maximilian Gerhardt

Да, мне нужны случайные числа для криптографически безопасных приложений., @Rexcirus


4 ответа


Лучший ответ:

9

randomSeed(analogRead(x)) создаст только 255 последовательностей чисел, что делает тривиальной проверку всех комбинаций и создание оракула, который может соединиться с вашим выходным потоком, предсказывая все выходные данные 100 %. Однако вы на правильном пути, это всего лишь игра с числами, и вам нужно их НАМНОГО больше. Например, взять 100 аналоговых чтений из 4 АЦП, суммировать их все и передать в randomSeed, было бы намного лучше. Для максимальной безопасности вам нужен как непредсказуемый ввод, так и недетерминированное смешивание.

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

Непредсказуемый ввод:

  • analogRead() (для плавающих контактов)
  • ПолучитьВремя()

Потенциально непредсказуемый ввод:

  • micros() (с недетерминированным периодом выборки)
  • дрожание часов (низкая пропускная способность, но пригодная для использования)
  • readVCC() (если не работает от батареи)

Внешний непредсказуемый ввод:

  • датчики температуры, влажности и давления
  • микрофоны
  • Делители напряжения LDR
  • шум транзистора с обратным смещением
  • дрожание компаса/ускорения
  • сканирование точки доступа Wi-Fi esp8266 (ssid, db и т. д.)
  • время esp8266 (фоновые задачи Wi-Fi делают запланированные выборки micros() неопределенными)
  • esp8266 HWRNG – RANDOM_REG32 – чрезвычайно быстрый и непредсказуемый, 1-ступенчатый

собирать Последнее, что вы хотите сделать, это выплевывать энтропию по мере ее поступления. Легче угадать бросок монеты, чем ведро монет. Суммирование хорошее. unsigned long bank; затем позже bank+= thisSample; хорошо; он перевернется. bank[32] еще лучше, читайте дальше. Вы хотите собрать как минимум 8 выборок входных данных для каждого фрагмента выходных данных, в идеале гораздо больше.

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

Вот почему вы хотите сохранить некоторую энтропию в долгосрочной перспективе, используя EEPROM, SD и т. д. Посмотрите Fortuna PRNG использует 32 банка, каждый из которых обновляется в два раза реже, чем предыдущий. Это затрудняет атаку всех 32 банков за разумное время.

Обработка После того, как вы соберете «энтропию», вы должны очистить ее и отделить от ввода труднообратимым способом. SHA/1/256 подходит для этого. Вы можете использовать SHA1 (или даже MD5) для скорости, поскольку у вас нет уязвимости открытого текста. Чтобы собрать урожай, никогда не используйте полный банк энтопии и ВСЕГДА ВСЕГДА добавляйте «соль» к выходным данным, которая каждый раз разная, чтобы предотвратить идентичные выходные данные при отсутствии изменений в банке энтропии: output = sha1( String(micros()) + String (bank[0]) + [...] ); Функция sha одновременно скрывает входные данные и отбеливает выходные данные, защищая от слабых семян, низкого накопления ent и других распространенных проблем.

Чтобы использовать входные данные таймера, необходимо сделать их недетерминированными. Это просто как delayMicroseconds(lastSample % 255); который делает паузу на непредсказуемое количество времени, делая «последовательные» показания часов неравномерными по различию. Делайте это полурегулярно, например if(analogRead(A1)>200){...}, при условии, что A1 зашумлен или подключен к динамическому вводу. Усложнение определения каждой вилки вашего потока предотвратит криптоанализ декомпилированного/скопированного вывода.

Настоящая безопасность — это когда злоумышленник знает всю вашу систему и все еще беспомощен в ее преодолении.

Наконец, проверьте свою работу. Пропустите свой вывод через ENT.EXE (также доступен для nix/mac) и посмотрите, подходит ли он. Наиболее важным является распределение хи-квадрат, которое обычно должно составлять от 33% до 66%. Если вы получаете 1,43% или 99,999% или что-то в этом роде, более одного теста подряд, ваш рандом — дерьмо. Вы также хотите, чтобы энтропия ENT сообщала как можно ближе к 8 битам на байт, > 7,9 наверняка.

TLDR: Самый простой надежный способ — использовать HWRNG ESP8266. Это быстро, равномерно и непредсказуемо. Запустите что-то подобное на ESP8266 с ядром Ardunio и используйте последовательный порт для связи с AVR:

// Код ядра ESP8266 Arduino:
void setup(){
 Serial.begin(9600); // или как там
}

void loop() {
  // Serial.write((char)(RANDOM_REG32 % 256)); // "бункер"
  Serial.print( String(RANDOM_REG32, HEX).substring(1)); // "шестнадцатеричный"
}

** изменить

вот скетч HWRNG на голой плате, который я написал некоторое время назад, работающий не только как сборщик, но и как целый CSPRNG, выплевывающий из последовательного порта. Он создан для про-мини, но должен быть легко адаптирован к другим платам. Можно использовать просто плавающие аналоговые пины, но лучше добавить к ним что-нибудь, желательно разное. Например, микрофоны, фоторезисторы, термисторы (урезанные до максимальной температуры в помещении) и даже длинные провода. Он довольно хорошо работает в ЛОР, если у вас есть даже умеренный шум.

Скетч объединяет несколько понятий, которые я упомянул в своем ответе и последующих комментариях: накопление энтропии, растяжение за счет избыточной выборки далеко не идеальной энтропии (фон Нейман сказал, что это круто) и хеширование до единообразия. Он отказывается от энтропийной оценки качества в пользу «дайте мне что-нибудь, возможно, динамическое» и смешивания с использованием криптографического примитива.

// AVR (ardunio) HWRNG от dandavis. выпущен в общественное достояние автором.
#include <Hash.h> 

unsigned long read[8] = {0, 0, 0, 0, 0, 0, 0, 0};
const int pincount = 9; // уменьшаем для плат не pro-mini
int pins[9] = {A0, A1, A2, A3, A4, A5, A6, A7, A0}; // настройка для платы, имя аналоговых входов для выборки
unsigned int ticks = 0;
String buff = ""; // содержит один раунд токенов деривации для хеширования.
String cache; // последний прочитанный хэш



void harvest() { // String() замедляет обработку, что затрудняет воссоздание вызовов micros()
  unsigned long tot = 0; // сумма всех аналоговых операций чтения
  buff = String(random(2147483647)) + String(millis() % 999);
  int seed =  random(256) + (micros() % 32);
  int offset =  random(2147483647) % 256;

  for (int i = 0; i < 8; i++) {
    buff += String( seed + read[i] + i + (ticks % 65), HEX );
    buff += String(random(2147483647), HEX);
    tot += read[i];
  }//следующее я

  buff += String( (micros() + ticks + offset) % 99999, HEX);
  if (random(10) < 3) randomSeed(tot + random(2147483647) + micros()); 
  buff = sha1( String(random(2147483647)) + buff + (micros()%64) + cache); // используется хэш для унифицированного вывода и пустой траты времени
  Serial.print( buff ); // выводим хэш
  cache = buff;
  spin();
}//конец сбора()


void spin() { // добавляем энтропию и перемешиваем
  ticks++;
  int sample = 128;
  for (int i = 0; i < 8; i++) { // обновляем ~6/8 банков 8 раз
    read[ read[i] % 8] += (micros() % 128);
    sample = analogRead(  pins[i] ); // чтение с каждого аналогового вывода
    read[ micros() % 8] += ( read[i] % 64 ); // смешать синхронизацию и 6LSB из чтения
    read[i] += sample; // смешать весь необработанный образец
    read[(i + 1) % 8] += random(2147483647) % 1024; // смешиваем прнг
    read[ticks % 8] += sample % 16; // смешиваем лучшую часть прочитанного
    read[sample % 8] += read[ticks % 8] % 2147483647; // внутримиксовые банки
  }

}//конец вращения()



void setup() {
  Serial.begin(9600);
  delay(222);
  int mx = 2028 + ((analogRead(A0)  + analogRead(A1) + analogRead(A2)  + analogRead(A3)) % 256);  
  while (ticks < mx) {
    spin();
    delay(1);
    randomSeed(read[2] + read[1] + read[0] + micros() + random(4096) + ticks);
  }// идем
}// конец настройки()



void loop() {
  spin();
  delayMicroseconds((read[ micros() % 8] %  2048) + 333  );
  delay(random(10));
  //если (millis() < 500) return;
  if ((ticks % 16) == (millis() % 16) ) harvest();
}// конец цикла()
,

(У меня здесь мало персонажей, извините.) Хороший обзор! Я бы посоветовал использовать счетчик соли; micros() — это пустая трата битов, поскольку между вызовами она может перемещаться на несколько шагов. Избегайте старших битов на аналоговых входах, ограничьтесь одним или двумя младшими битами. Даже при целенаправленной атаке их трудно обнаружить (если только вы не сможете подключить провод ко входу). «Недетерминированное смешивание» — это не то, что можно сделать в программном обеспечении. Смешивание SHA-1 стандартизировано: https://crypto.stackexchange.com/a/6232. Индет. Предложенный вами таймер настолько же случайен, насколько и источник, который у вас уже есть. Здесь особой выгоды нет., @Jonas Schäfer

sha упрощает и защищает, так что вам не придется беспокоиться, например, о том, сколько бит нужно получить с аналогового входа. несколько дюймов провода, подключенного к аналогу (или змеевидной дорожке печатной платы), будут раскачивать его больше, чем на несколько бит. смешивание является недетерминированным из-за того, что несохраненная и неизвестная соль подается в хэш с подвыборкой накопленных значений. micros() сложнее воспроизвести, чем счетчик, особенно при запуске с недетерминированными интервалами., @dandavis

У меня есть вопрос. Вы сказали, что лучше принять 100 мер. Но не является ли принятие множества мер своего рода «средним», которое ограничивает эффективность сбора этих «случайных» данных? Я имею в виду, что обычно вы усредняете, чтобы получить менее шумные (то есть менее «случайные») измерения…, @frarugi87

ну, я рекомендую постоянную выборку, я просто сказал, что 100 лучше, чем 1, поскольку оно предлагает больше комбинаций. Модель накопления, такая как Ярроу/Фортуна, по-прежнему намного лучше. Рассмотрите возможность объединения (не суммирования) этих 100 аналоговых выборок перед хешированием; сильнее, потому что это делает порядок выборки важным, а отсутствие одного символа дает совершенно другой хэш. Таким образом, даже несмотря на то, что можно усреднить выборки, чтобы получить меньше шума, злоумышленнику придется дословно перечислить все значения или не найти совпадений... Моя основная идея - «накопить, смешать и проверить», а не отстаивать конкретный источник шума., @dandavis


9

В библиотеке Entropy используются:

естественное дрожание сторожевого таймера для создания надежного потока истинных случайных чисел

Мне нравится это решение, потому что оно не использует контакты вашего микроконтроллера и не требует никаких внешних схем. Это также делает его менее подверженным внешним сбоям.

В дополнение к библиотеке они также предоставляют скетч, демонстрирующий использование той же техники, что и для генерации случайного начального числа для PRNG микроконтроллера без библиотеки: https://sites.google. com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed

,

3

По моему опыту, analogRead() на плавающем выводе имеет очень низкую энтропия. Может быть, один или два бита случайности на звонок. Вы определенно хочется чего-то лучшего. Джиттер сторожевого таймера, предложенный в ответ per1234 - хорошая альтернатива. Однако он порождает энтропию с довольно медленной скоростью, что может быть проблемой, если вам это нужно в нужное время программа запускается. у Дандависа есть несколько хороших предложений, но они обычно требуется либо ESP8266, либо внешнее оборудование.

Есть один интересный источник энтропии, который еще не упоминался: содержимое неинициализированной оперативной памяти. Когда MCU включен, некоторые битов ОЗУ (имеющих наиболее симметричную транзисторы) запускаются в случайном состоянии. Как обсуждалось в этом научная статья, ее можно использовать как источник энтропии. Это только доступен при холодной загрузке, поэтому вы можете использовать его для заполнения начальной энтропии пул, который вы бы потом периодически пополняли из другого, потенциально медленный источник. Таким образом, ваша программа может начать свою работу не дожидаясь, пока бассейн медленно наполнится.

Вот пример того, как эти данные могут быть собраны на основе AVR. Ардуино. Фрагмент кода ниже выполняет операцию XOR всей оперативной памяти, чтобы построить seed, который позже передается в srandom(). Сложность заключается в том, что сбор должен быть выполнен до среды выполнения C инициализирует .data и .bss разделы памяти, а потом сид приходится сохранять в место среда выполнения C не будет перезаписывать. Это делается с помощью специальных разделы памяти.

uint32_t __attribute__((section(".noinit"))) random_seed;

void __attribute__((naked, section(".init3"))) seed_from_ram()
{
    const uint32_t * const ramstart = (uint32_t *) RAMSTART;
    const uint32_t * const ramend   = (uint32_t *) RAMEND;
    uint32_t seed = 0;
    for (const uint32_t *p = ramstart; p <= ramend; p++)
        seed ^= *p;
    random_seed = seed;
}

void setup()
{
    srandom(random_seed);
}

Обратите внимание, что при теплом сбросе SRAM сохраняется, поэтому в ней по-прежнему все содержимое вашего пула энтропии. Затем этот же код можно использовать для сохранить накопленную энтропию при сбросе.

Изменить: исправлена проблема в моей начальной версии seed_from_ram(), которая работал с глобальным random_seed вместо использования локального seed. Это может привести к тому, что семя будет подвергнуто операции XOR с самим собой, что уничтожит все собранная энтропия.

,

Хорошая работа! я могу украсть? re: контакты: достаточно одного или двух неизвестных битов, если они используются правильно; это ограничило бы только скорость вывода совершенной секретности (гадость), но не вычислительную секретность, которая нам нужна..., @dandavis

@dandavis: Да, вы можете использовать повторно, конечно. Вы правы в том, что analogRead() можно использовать, если вы знаете, что делаете. Вам просто нужно быть осторожным, чтобы не переоценить его случайность при обновлении оценки энтропии вашего пула. Моя точка зрения на analogRead() в основном предназначена для критики плохого, но [часто повторяемого «рецепта»](https://www.arduino.cc/reference/en/language/functions/random-numbers/randomseed/ ): randomSeed(analogRead(0)) _только один раз_ в setup() и предположим, что этого достаточно., @Edgar Bonet

Если analogRead(0) имеет 1 бит энтропии на вызов, то его повторный вызов даст 10000/8 = 1,25 КБ/с энтропии, что в 150 раз больше, чем у библиотеки энтропии., @Dmitry Grigoryev


0

Если вам на самом деле не нужна энтропия и вы просто хотите получать разные последовательности псевдослучайных чисел при каждом запуске, вы можете использовать EEPROM для перебора последовательных начальных чисел. Технически процесс будет полностью детерминированным, но с практической точки зрения он намного лучше, чем randomSeed(analogRead(0)) на неподключенном выводе, который часто заставит вас начать с одного и того же начального числа либо 0, либо 1023. Сохранение следующего начального числа в EEPROM гарантирует, что вы каждый раз будете начинать с нового начального числа.

#include <EEPROM.h>

const int seed_addr = 0;
unsigned long seed;

void setup() {
    seed = EEPROM.read(seed_addr);
    EEPROM.write(seed_addr, seed+1);
    randomSeed(seed);
}

Если вам нужна реальная энтропия, вы можете получить ее либо из дрейфа часов, либо путем усиления внешнего шума. И если вам нужно много энтропии, внешний шум — единственный приемлемый вариант. Стабилитрон является популярным выбором, особенно если у вас есть источник напряжения выше 5-6 В (он будет работать и с 5 В с соответствующим стабилитроном, но будет производить меньшую энтропию):

(источник).

Выход усилителя должен быть подключен к аналоговому выводу, который будет производить несколько битов энтропии с каждым analogRead() до десятков МГц (быстрее, чем может производить выборка Arduino).

,