在Arduino中获得真正的随机数


13

在Arduino中获得真实(而不是伪)随机数的最佳方法是什么,或者至少是最佳近似方法是什么?据我了解,函数randomSeed(analogRead(x))不够随机。

如果可能的话,该方法应仅利用基本的Arduino设置(无需额外的传感器)。如果使用外部传感器的解决方案大大改善了基本设置的随机性,则欢迎使用。


有什么用途?它必须是加密安全的吗?那你随便怎么办?然后,如果没有外部芯片从物理熵源实现TRNG,您将很不走运。您还可以实现HMAC DRBG之类的确定性RNG,并从静态的东西和低质量的熵源中获取它的种子,但是这在密码学上仍然不是安全的。
Maximilian Gerhardt

是的,对于加密安全的应用程序,我需要随机数。
Rexcirus

Answers:


10

熵库的用途:

看门狗定时器的自然抖动,以产生可靠的真实随机数流

我喜欢这个解决方案,因为它不占用您微控制器的任何引脚,也不需要任何外部电路。这也使得它较少受到外部故障的影响。

除了一个库,他们还提供了一个草图,演示了不使用库就可以为微控制器的PRNG生成随机种子的相同技术的用法:https : //sites.google.com/site/astudyofentropy/project-definition /计时器抖动熵源/熵库/ arduino随机种子


8

randomSeed(analogRead(x))只会产生255个数字序列,这使得尝试所有连击并产生可以耦合到您的输出流的预言机变得微不足道,从而可以预测所有输出100%。但是,您的方向正确,这只是一个数字游戏,您还需要更多。例如,从4个ADC进行100次模拟读取,将它们全部求和,然后馈给它们randomSeed会更好。为了获得最大的安全性,您既需要不可预测的输入又需要不确定的混合。

我不是密码学家,但是我花了数千个小时研究和构建硬件和软件随机生成器,所以让我分享一些我学到的知识:

不可预测的输入:

  • AnalogRead()(在浮动引脚上)
  • GetTemp()

潜在的不可预测的输入:

  • micros()(带有不确定的采样周期)
  • 时钟抖动(低带宽,但可用)
  • readVCC()(如果不是电池供电)

外部不可预测的输入:

  • 温度,湿度和压力传感器
  • 麦克风
  • LDR分压器
  • 反向偏置晶体管噪声
  • 指南针/加速度抖动
  • esp8266 wifi热点扫描(ssid,db等)
  • esp8266计时(后台wifi任务使预定的micros()不确定获取)
  • esp8266 HWRNG- RANDOM_REG32极快且不可预测的一站式

收集 您想要做的最后一件事就是随即吐出熵。猜测硬币翻转比一桶硬币容易。总结是好的。unsigned long bank;那以后bank+= thisSample;好 它会翻转。bank[32]更好,请继续阅读。您希望为每个输出块至少收集8个输入样本,最好是更多。

防止中毒 如果加热电路板会导致一定的最大时钟抖动,这就是攻击向量。与将RFI吹向AnalogRead()输入相同。另一种常见的攻击是拔掉设备的电源,从而丢弃所有累积的熵。您不应该输出数字,除非您知道这样做是安全的,即使这样做会牺牲速度。

这就是为什么您要使用EEPROM,SD等使熵长期保持不变的原因。研究Fortuna PRNG,它使用32个存储库,每个存储库的更新频率是之前存储库的一半。这使得很难在合理的时间内攻击所有32个银行。

处理 一旦收集到“熵”,就必须清除它,并以一种难以逆转的方式将其与输入分离。SHA / 1/256对此很有用。由于没有明文漏洞,因此可以使用SHA1(甚至是MD5)来提高速度。收获时,切勿使用完整的底物库,并且始终在每次输出时都添加一个“盐”,以防止相同的输出,因为没有熵库变化:output = sha1( String(micros()) + String(bank[0]) + [...] );sha功能既可隐藏输入也可白化输出,以防止种子变弱,耳鼻喉科的累积率低,以及其他常见问题

要使用计时器输入,需要使它们不确定。这很简单delayMicroseconds(lastSample % 255);这会暂停不可预测的时间,从而使“连续”时钟读取的差异不一致。像if(analogRead(A1)>200){...}A 一样,半定期执行此操作,前提是A1嘈杂或挂接到动态输入。使流程的每个分支很难确定将阻止对反编译/翻录输出的加密分析。

真正的安全是当攻击者知道您的整个系统并且仍然无能为力时才能克服它。

最后,检查您的工作。通过ENT.EXE(也可用于nix / mac)运行您的输出,并查看它是否有用。最重要的是卡方分布,通常应在33%至66%之间。如果您获得1.43%或99.999%或类似的毛病,连续进行多个测试,那么您的随机性就是垃圾。您还希望熵ENT报告每字节尽可能接近8位,肯定大于7.9。

TLDR:最简单的防呆方法是使用ESP8266的HWRNG。快速,统一且不可预测。在运行Ardunio核心的ESP8266上运行类似的操作,并使用串行与AVR通讯:

// ESP8266 Arduino core code:
void setup(){
 Serial.begin(9600); // or whatever
}

void loop() {
  // Serial.write((char)(RANDOM_REG32 % 256)); // "bin"
  Serial.print( String(RANDOM_REG32, HEX).substring(1)); // "hex"
}

**编辑

这是我不久前写的一个裸露的HWRNG草图,它不仅用作收集器,而且还用作整个CSPRNG,从串行端口喷出。它是为专业人士而设计的,但应易于适应其他主板。您可以只使用浮动模拟引脚,但最好向它们添加内容,最好是添加不同的内容。像麦克风,LDR,热敏电阻(在室温下最大程度地散布),甚至是长电线。如果您有适度的噪音,它在ENT中的效果也很好。

该草图整合了我在回答和后续评论中提到的几个概念:积累熵,通过对不理想的熵进行过采样来拉伸(冯·诺伊曼说这很酷)以及哈希化为均匀性。它放弃了熵质量估计,而支持“给动态添加任何可能的东西”,并使用密码原语进行混合。

// AVR (ardunio) HWRNG by dandavis. released to public domain by author.
#include <Hash.h> 

unsigned long read[8] = {0, 0, 0, 0, 0, 0, 0, 0};
const int pincount = 9; // adjust down for non pro-mini boards
int pins[9] = {A0, A1, A2, A3, A4, A5, A6, A7, A0}; // adjust for board, name analog inputs to be sampled
unsigned int ticks = 0;
String buff = ""; // holds one round of derivation tokens to be hashed.
String cache; // the last read hash



void harvest() { // String() slows down the processing, making micros() calls harder to recreate
  unsigned long tot = 0; // the total of all analog reads
  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];
  }//next 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); // used hash to uniform output and waste time
  Serial.print( buff ); // output the hash
  cache = buff;
  spin();
}//end harvest()


void spin() { // add entropy and mix
  ticks++;
  int sample = 128;
  for (int i = 0; i < 8; i++) { // update ~6/8 banks 8 times
    read[ read[i] % 8] += (micros() % 128);
    sample = analogRead(  pins[i] ); // a read from each analog pin
    read[ micros() % 8] += ( read[i] % 64 ); // mix timing and 6LSBs from read
    read[i] += sample; // mix whole raw sample
    read[(i + 1) % 8] += random(2147483647) % 1024; // mix prng
    read[ticks % 8] += sample % 16; // mix the best nibble of the read
    read[sample % 8] += read[ticks % 8] % 2147483647; // intra-mix banks
  }

}//end spin()



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);
  }// wend
}// end setup()



void loop() {
  spin();
  delayMicroseconds((read[ micros() % 8] %  2048) + 333  );
  delay(random(10));
  //if (millis() < 500) return;
  if ((ticks % 16) == (millis() % 16) ) harvest();
}// end loop()

(抱歉,我的字符短缺。)概述不错!我建议用一个盐柜台。micros()浪费了一些位,因为它在调用之间可能会跳几步。避免模​​拟输入中的高位,限制到最低的一位或两位。即使是有针对性的攻击,也很难确定(除非您可以在Input上加一根电线)。您无法在软件中执行“非确定性混合”。SHA-1混合已标准化:crypto.stackexchange.com/a/6232。indet。您建议的计时器仅与您已有的来源一样随机。在这里没有太大的收获。
乔纳斯舍费尔

sha进行了简化和保护,因此您不必担心例如要从模拟输入中抓取多少位的问题。与模拟信号(或蜿蜒的pcb走线)连接几英寸的电线,会使它的摆动幅度超过几位。混合是不确定的,因为未保存的盐和未知的盐会通过累积值的子样本馈入哈希。micros()比计数器更难重播,尤其是在不确定的时间间隔触发时。
丹达维斯

1
我有个问题。您说采取100措施更好。但是,采取大量措施难道不是一种限制了获取这些“随机”数据有效性的“平均值”吗?我的意思是,通常情况下,您平均会得到较少的噪声(因此较少的“随机”)测量结果
frarugi87 '18

好吧,我建议您进行恒定采样,我只是说100比1好,因为它提供了更多的组合。像Yarrow / Fortuna这样的积累模型仍然要好得多。考虑在散列之前将这100个模拟样本串联(而不是求和);更强大,因为它使样本顺序变得重要,而减少一个字符将产生完全不同的哈希。因此,即使可以平均采样以减少噪声,攻击者也必须逐字背诵所有值或不匹配……我的主要观点是“积累,混合和验证”,而不是提倡特定的噪声源。
丹达维斯

4

根据我的经验,analogRead()在浮动引脚上的熵非常低。每次通话可能只有一两个比特的随机性。您肯定想要更好的东西。根据per1234的回答,看门狗定时器的抖动是一个很好的选择。但是,它以非常慢的速率生成熵,如果在程序启动时需要它,这可能是一个问题。dandavis有很多不错的建议,但它们通常需要ESP8266或外部硬件。

有一个有趣的熵源尚未被提及:未初始化RAM的内容。当MCU上电时,其某些RAM位(那些恰好具有最对称的晶体管)以随机状态启动。就像在这篇hackaday文章中讨论的 那样,它可以用作熵源。它仅在冷启动时可用,因此您可以使用它来填充初始熵池,然后定期从另一个可能很慢的源中对其进行补充。这样,您的程序就可以开始工作,而不必等待池慢慢填满。

这是如何在基于AVR的Arduino上进行采集的示例。下面的代码段对整个RAM进行了XOR运算,以建立一个种子,稍后将其提供给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。这可能导致种子与其自身进行异或运算,从而破坏了迄今为止收获的所有熵。


干得好!我可以偷吗?针:如果正确使用,一或两个未知数就足够了;那只会限制完美保密性(运气)的输出速度,而不会限制我们需要的计算保密性……
dandavis

1
@dandavis:是的,可以重复使用。analogRead()如果您知道自己在做什么,那么使用可用性是正确的。您只需要注意在更新池熵的估计时不要高估其随机性。我约点analogRead()主要是指作为一个贫穷但的批评经常重复“配方”randomSeed(analogRead(0)) 只是一次setup()和假设它的足够。
埃德加·博内特

如果analogRead(0)每个调用具有1位熵,那么重复调用将产生10000/8 = 1.25 KB /秒的熵,是熵库的150倍。
德米特里·格里戈里耶夫

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-6V(使用适当的齐纳二极管也可以在5V下工作,但产生的熵较少):

在此处输入图片说明

来源)。

放大器的输出必须连接到模拟引脚,该引脚将产生几比特的熵,每个比特的analogRead()最高十兆赫兹(比Arduino采样速度快)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.