Ottenere un numero davvero casuale in Arduino


13

Qual è il metodo migliore per ottenere un numero veramente (al contrario di pseudo) casuale in Arduino, o almeno la migliore approssimazione possibile? Da quanto ho capito, la funzione randomSeed (analogRead (x)) non è abbastanza casuale.

Se possibile, il metodo dovrebbe sfruttare solo la configurazione di base di Arduino (senza sensori aggiuntivi). Le soluzioni con sensori esterni sono benvenute se migliorano significativamente la casualità rispetto all'impostazione di base.


Qual è l'applicazione? Deve essere crittograficamente sicuro? Cosa fai allora con la casualità? Quindi senza un chip esterno che implementa un TRNG da una fonte di entropia fisica, sei sfortunato. Potresti anche implementare un RNG deterministico come un DRBG HMAC e seminarlo da qualcosa di statico più una fonte di entropia di bassa qualità, ma che non sarà ancora sicuro crittograficamente.
Maximilian Gerhardt,

Sì, ho bisogno di numeri casuali per applicazioni crittograficamente sicure.
Rexcirus

Risposte:


10

La libreria Entropy utilizza:

il jitter naturale del timer watchdog per produrre un flusso affidabile di numeri casuali reali

Mi piace questa soluzione perché non utilizza alcun pin del microcontrollore e non richiede alcun circuito esterno. Ciò lo rende anche meno soggetto a guasti esterni.

Oltre a una libreria, forniscono anche uno schizzo che dimostra l'uso della stessa tecnica utilizzata per generare un seme casuale per il PRNG del microcontrollore senza la libreria: https://sites.google.com/site/astudyofentropy/project-definition / timer-jitter-entropia-sources / entropia-library / arduino-random-seed


8

randomSeed(analogRead(x))produrrà solo 255 sequenze di numeri, il che rende banale provare tutte le combo e produrre un oracolo che può accoppiarsi al flusso di output, prevedendo tutto l'output al 100%. Tuttavia, sei sulla buona strada, è solo un gioco di numeri e ne hai bisogno MOLTO di più. Ad esempio, prendere 100 letture analogiche da 4 ADC, sommarle tutte e darle da mangiare randomSeedsarebbe molto meglio. Per la massima sicurezza, sono necessari sia input imprevedibili sia missaggi non deterministici.

Non sono un crittografo, ma ho trascorso migliaia di ore a ricercare e costruire generatori casuali di hardware e software, quindi lasciami condividere alcune delle cose che ho imparato:

Input imprevedibile:

  • analogRead () (su pin flottanti)
  • GetTemp ()

Input potenzialmente imprevedibile:

  • micros () (con un periodo di campionamento non deterministico)
  • clock jitter (larghezza di banda ridotta, ma utilizzabile)
  • readVCC () (se non alimentato a batteria)

Ingresso imprevedibile esterno:

  • sensori di temperatura, umidità e pressione
  • microfoni
  • Divisori di tensione LDR
  • rumore del transistor a polarizzazione inversa
  • jitter bussola / accelerazione
  • scansione hotspot wifi esp8266 (ssid, db, ecc)
  • temporizzazione esp8266 (le attività wifi in background rendono i micros () recuperati indeterminati)
  • esp8266 HWRNG - RANDOM_REG32-molto veloce e imprevedibile, a 1 stop

collezionare L'ultima cosa che vuoi fare è sputare entropia man mano che arriva. È più facile indovinare un lancio della moneta che un secchio di monete. La somma è buona. unsigned long bank;poi più tardi bank+= thisSample;va bene; rotolerà. bank[32]è ancora meglio, continua a leggere. Volete raccogliere almeno 8 campioni di input per ogni blocco di output, idealmente molto di più.

Proteggersi dall'avvelenamento Se il riscaldamento della scheda provoca un certo jitter di clock massimo, questo è un vettore di attacco. Lo stesso vale per l'RMI di sabbiatura verso gli ingressi analogRead (). Un altro attacco comune semplicemente scollega l'unità scaricando così tutta l'entropia accumulata. Non dovresti produrre numeri finché non sai che è sicuro farlo, anche a costo della velocità.

Questo è il motivo per cui vuoi mantenere un po 'di entropia a lungo termine, usando EEPROM, SD, ecc. Guarda il PRNG Fortuna , che utilizza 32 banche, ognuna aggiornata la metà di quella precedente. Ciò rende difficile attaccare tutte e 32 le banche in un ragionevole lasso di tempo.

Elaborazione Una volta raccolta "entropia", è necessario ripulirla e separarla dall'input in modo difficile da invertire. SHA / 1/256 è buono per questo. Puoi usare SHA1 (o addirittura MD5 davvero) per la velocità poiché non hai una vulnerabilità in chiaro. Per raccogliere, non utilizzare mai il banco full entopy e SEMPRE aggiungere SEMPRE un "sale" all'output che è ogni volta diverso per evitare output identici dato nessun cambiamento di banca entropica: output = sha1( String(micros()) + String(bank[0]) + [...] );la funzione sha nasconde input e imbianca output, proteggendo da semi deboli, basso accumulo ent e altri problemi comuni.

Per utilizzare gli ingressi timer, è necessario renderli indeterministici. Questo è un semplice delayMicroseconds(lastSample % 255); che mette in pausa un periodo di tempo imprevedibile, rendendo l'orologio "successivo" legge differenze non uniformi. Fatelo semi-regolarmente, ad esempio if(analogRead(A1)>200){...}, a condizione che A1 sia rumoroso o agganciato a un input dinamico. Rendere piuttosto difficile determinare ogni fork del flusso impedirà la crittoanalisi sull'output decompilato / rippato.

La vera sicurezza è quando l'attaccante conosce l'intero sistema ed è ancora impotente per superarlo.

Infine, controlla il tuo lavoro. Esegui l'output tramite ENT.EXE (disponibile anche per nix / mac) e verifica se funziona. La più importante è la distribuzione del chi quadro, che dovrebbe essere compresa tra il 33% e il 66%. Se ottieni l'1,43% o il 99,999% o qualcosa di simile, più di un test di fila, il tuo caso è una schifezza. Volete anche che l'ENTrop entri nel rapporto il più vicino possibile a 8 bit per byte,> 7.9 di sicuro.

TLDR: il modo più semplice a prova di folle è l'HWRNG dell'ESP8266. È veloce, uniforme e imprevedibile. Esegui qualcosa del genere su un ESP8266 che esegue il core Ardunio e usa il seriale per parlare con l'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"
}

** modificare

ecco uno schizzo HWRNG a bordo nudo che ho scritto qualche tempo fa, operando non solo come un collezionista, ma come un intero CSPRNG che sputa fuori dalla porta seriale. È costruito per un pro-mini ma dovrebbe essere facilmente adattabile ad altre schede. Puoi usare solo pin analogici flottanti, ma è meglio aggiungere cose, preferibilmente cose diverse. Come microfoni, LDR, termistori (tagliati alla massima diffusione attorno alla temperatura ambiente) e persino fili lunghi. Fa abbastanza bene in ENT se hai anche un rumore moderato.

Lo schizzo integra diverse nozioni che ho citato nella mia risposta e nei commenti di follow-up: accumulare entropia, allungando per eccesso di campionamento entropia non ideale (von Neumann ha detto che è bello) e hash per l'uniformità. Rinuncia alla stima della qualità dell'entropia a favore di "dammi qualsiasi cosa possibilmente dinamica" e mescolandosi usando una primitiva crittografica.

// 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()

(Sono a corto di personaggi qui, scusa.) Buona panoramica! Suggerirei di usare un contatore per il sale; micros () è uno spreco di bit perché può saltare di diversi passaggi tra le chiamate. Evitare i bit alti negli ingressi analogici, limitarsi a uno o due bit più bassi. Anche con un attacco mirato sono difficili da individuare (a meno che tu non possa mettere un filo sull'Input). La "miscelazione non deterministica" non è qualcosa che puoi fare nel software. Il mixaggio SHA-1 è standardizzato: crypto.stackexchange.com/a/6232 . L'indet. il timer che proponi è casuale come la fonte che hai già. Non c'è molto guadagno qui.
Jonas Schäfer,

sha semplifica e protegge, in modo da non doverti preoccupare di quanti bit prelevare da un ingresso analogico, ad esempio. qualche centimetro di filo collegato ad un analogo (o una traccia a serpentina per pcb) lo farà oscillare più di qualche bit. la miscelazione non è deterministica in virtù del sale non salvato e sconosciuto alimentato all'hash con un sottocampione di valori accumulati. micros () è più difficile da riprodurre rispetto a un contatore, specialmente se sparato a intervalli non deterministici.
dandavis,

1
Ho una domanda. Hai detto che prendere 100 misure è meglio. Ma prendere molte misure non è una sorta di "media" che limita l'efficacia di questi dati "casuali"? Voglio dire, di solito hai una media per ottenere misurazioni meno rumorose (quindi meno "casuali") ...
frarugi87

beh, raccomando un campionamento costante, stavo solo dicendo che 100 è meglio di 1 poiché offre più combinazioni. Un modello di accumulazione come Yarrow / Fortuna è ancora di gran lunga migliore. Valuta di concatenare (non sommare) quei 100 campioni analogici prima dell'hash; più forte perché rende l'ordine del campione importante, ed essere a un solo carattere produce un hash completamente diverso. Quindi, anche se si potrebbe fare la media dei campioni per ottenere meno rumore, un utente malintenzionato dovrebbe recitare letteralmente tutti i valori o nessuna corrispondenza ... Il mio punto principale è "accumulare, mescolare e verificare" più che sostenere una specifica fonte di rumore.
dandavis,

4

Dalla mia esperienza, analogRead()su un perno mobile ha un'entropia molto bassa. Forse uno o due bit di casualità per chiamata. Sicuramente vuoi qualcosa di meglio. Il jitter del timer del watchdog, come proposto nella risposta di per1234, è una buona alternativa. Tuttavia, genera entropia a un ritmo piuttosto lento, che può essere un problema se ne hai bisogno proprio all'avvio del programma. dandavis ha alcuni buoni suggerimenti, ma generalmente richiedono un ESP8266 o un hardware esterno.

C'è un'interessante fonte di entropia che non è stata ancora menzionata: il contenuto della RAM non inizializzata. Quando l'MCU è acceso, alcuni dei suoi bit RAM (quelli che hanno i transistor più simmetrici) si avviano in uno stato casuale. Come discusso in questo articolo di hackaday , questo può essere usato come fonte di entropia. È disponibile solo a avvio a freddo, quindi è possibile utilizzarlo per riempire un pool di entropia iniziale, che verrebbe quindi periodicamente rifornito da un'altra fonte potenzialmente lenta. In questo modo il tuo programma può iniziare a funzionare senza dover attendere che il pool si riempia lentamente.

Ecco un esempio di come questo potrebbe essere raccolto su un Arduino basato su AVR. Lo snippet di codice sotto XOR alimenta l'intera RAM per creare un seme a cui successivamente si nutre srandom(). La parte difficile è che la raccolta deve essere effettuata prima che il runtime C inizializzi le sezioni di memoria .data e .bss, quindi il seme deve essere salvato in un punto in cui il runtime C non verrà sovrascritto. Questo viene fatto utilizzando sezioni di memoria specifiche .

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);
}

Si noti che, su un ripristino a caldo , la SRAM viene mantenuta, quindi ha ancora l'intero contenuto del pool di entropia. Questo stesso codice può quindi essere utilizzato per preservare l'entropia raccolta attraverso un reset.

Modifica : risolto un problema nella mia versione iniziale seed_from_ram()che funzionava sul globale random_seedinvece di utilizzare un locale seed. Ciò potrebbe portare al seme XORed con se stesso, distruggendo tutta l'entropia raccolta finora.


Bel lavoro! posso rubare? re: pins: uno o due bit sconosciuti sono sufficienti se utilizzati correttamente; ciò limiterebbe solo la velocità di uscita del segreto perfetto (schifo), ma non il segreto computazionale di cui abbiamo bisogno ...
dandavis

1
@dandavis: Sì, puoi riutilizzarlo, certo. Hai ragione a analogRead()essere utilizzabile se sai cosa stai facendo. Devi solo stare attento a non sopravvalutare la sua casualità quando aggiorni una stima dell'entropia del tuo pool. Il mio punto di vista analogRead()è principalmente inteso come una critica a una "ricetta" povera ma spesso ripetuta : randomSeed(analogRead(0)) basta una volta dentro setup()e supporre che sia abbastanza.
Edgar Bonet,

Se analogRead(0)ha 1 bit di entropia per chiamata, la sua chiamata ripetuta produrrà 10000/8 = 1,25 KByte / sec di entropia, 150 volte più della libreria Entropy.
Dmitry Grigoryev il

0

Se non hai davvero bisogno di entropia e vuoi semplicemente ottenere una diversa sequenza di numeri pseudo-casuali ad ogni avvio, puoi usare EEPROM per iterare attraverso seed consecutivi. Tecnicamente il processo sarà completamente deterministico, ma in termini pratici è molto meglio che randomSeed(analogRead(0))su un pin non collegato, che spesso ti farà iniziare con lo stesso seme di 0 o 1023. Salvare il seme successivo in EEPROM ti garantirà di iniziare con un diverso semina ogni volta.

#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);
}

Se hai bisogno di vera entropia, puoi raccoglierla dalla deriva dell'orologio o amplificando il rumore esterno. E se hai bisogno di molta entropia, il rumore esterno è l'unica opzione praticabile. Il diodo Zener è una scelta popolare, soprattutto se si dispone di una sorgente di tensione superiore a 5-6 V (funzionerà anche con 5 V con un diodo Zener appropriato, ma produrrà meno entropia):

inserisci qui la descrizione dell'immagine

( fonte ).

L'uscita dell'amplificatore deve essere collegata a un pin analogico, che produrrà diversi bit di entropia con ciascuno analogRead()fino a decine di MHz (più veloce di quanto Arduino possa campionare).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.