Perché utilizzare la classe C # System.Random invece di System.Security.Cryptography.RandomNumberGenerator?


87

Perché qualcuno dovrebbe utilizzare il generatore di numeri casuali "standard" di System.Random invece di utilizzare sempre il generatore di numeri casuali crittograficamente protetto da System.Security.Cryptography.RandomNumberGenerator (o le sue sottoclassi perché RandomNumberGenerator è astratto)?

Nate Lawson ci dice nella sua presentazione di Google Tech Talk " Crypto Strikes Back " al minuto 13:11 di non utilizzare i generatori di numeri casuali "standard" di Python, Java e C # e di utilizzare invece la versione crittograficamente sicura.

Conosco la differenza tra le due versioni di generatori di numeri casuali (vedi domanda 101337 ).

Ma quale logica c'è per non utilizzare sempre il generatore di numeri casuali sicuro? Perché usare System.Random? Forse le prestazioni?


7
Quale preferiresti digitare?
Macha

13
Troppe persone lo usano seriamente come giustificazione per quello che fanno (di solito non ad alta voce). Il codice viene letto più di quanto non sia scritto, a chi importa delle banali differenze di lunghezza?
Mark Sowul

3
Ma comunque perché dovresti usare RNG crittografici se non stai facendo crittografia?
Mark Sowul

3
@Macha, ecco a cosa servono gli alias ->using R = System.Security.Cryptography.RandomNumberGenerator; R.Create();
cchamberlain

Risposte:


147

Velocità e intento. Se stai generando un numero casuale e non hai bisogno di sicurezza, perché usare una funzione di crittografia lenta? Non hai bisogno di sicurezza, quindi perché far pensare a qualcun altro che il numero possa essere usato per qualcosa di sicuro quando non lo sarà?


31
Mi piace molto l'argomento dell'intento.
Lernkurve

12
Va notato che Random.GetNext è tutt'altro che bravo a "distribuire" i numeri casuali sullo spettro, specialmente in un ambiente a thread. Mi sono imbattuto in questo problema durante la scrittura di un programma per testare diverse soluzioni al problema Rand7 da Rand5. In un rapido test threaded appena ora di 100000 numeri casuali tra 0 e 10, 82470 dei numeri generati erano 0. Ho visto discrepanze simili nei miei test precedenti. La crittografia casuale è molto uniforme nella sua distribuzione dei numeri. Immagino che la lezione sia testare sempre i tuoi dati casuali per vedere che sono "abbastanza casuali" per le tue esigenze.
Kristoffer L

35
@Kristoffer Penso che tu abbia abusato Random. Fammi indovinare: hai creato una nuova istanza della Randomclasse per ogni numero, che poiché è seminato da un timer grossolano verrà seminato con lo stesso valore per un intervallo di circa 1-16 ms.
CodesInChaos

15
@CodesInChaos: Oltre a ciò, esiste una condizione di competizione Randomche fa sì che restituisca tutti gli 0 quando lo stesso oggetto viene utilizzato da più thread.
BlueRaja - Danny Pflughoeft

3
@KristofferL: Vedi il commento sopra, vedi anche questa risposta
BlueRaja - Danny Pflughoeft

66

Oltre alla velocità e all'interfaccia più utile ( NextDouble()ecc.), È anche possibile creare una sequenza casuale ripetibile utilizzando un valore seed fisso. Ciò è abbastanza utile, tra l'altro durante i test.

Random gen1 = new Random();     // auto seeded by the clock
Random gen2 = new Random(0);    // Next(10) always yields 7,8,7,5,2,....

2
E c'è il BitConverter.ToInt32 (Byte [] value, int startIndex) che potrebbe essere più facile da capire. ;)
sisve

7
Ian Bell e David Braben hanno utilizzato un generatore casuale nel gioco per computer Elite per creare un vasto elenco di pianeti e dei loro attributi (dimensioni, ecc.), Con una memoria molto limitata. Questo si basa anche sul generatore che crea un modello deterministico (da un seme) - che quello Crypto ovviamente non fornisce (in base alla progettazione). Ci sono altre informazioni su come lo hanno fatto qui: wiki.alioth.net/index.php / Random_number_generator e il libro "Infinite Game Universe: Mathematical Techniques" ISBN: 1584500581 ha una discussione più generale su tali tecniche.
Daniel James Bryars,


2
@phoog "Di conseguenza, il codice dell'applicazione non deve presumere che lo stesso seme risulterà nella stessa sequenza pseudocasuale in versioni diverse di .NET Framework." - Non lo so, mi sembra abbastanza chiaro. Tuttavia, non sarei sorpreso se non fosse possibile modificarlo in pratica senza interrompere i programmi esistenti, nonostante questo avvertimento.
Roman Starkov

2
@phoog: stai dicendo una cosa e poi l'esatto opposto. Ti stai contraddicendo direttamente.
Timwi

54

Prima di tutto la presentazione che hai collegato parla solo di numeri casuali per motivi di sicurezza. Quindi non pretende che Randomsia dannoso per scopi non di sicurezza.

Ma sostengo che lo sia. L'implementazione .net 4 di Randomè difettosa in diversi modi. Consiglio di usarlo solo se non ti interessa la qualità dei tuoi numeri casuali. Consiglio di utilizzare migliori implementazioni di terze parti.

Difetto 1: la semina

Il costruttore predefinito esegue il seed con l'ora corrente. Pertanto, tutte le istanze di Randomcreate con il costruttore predefinito in un breve lasso di tempo (circa 10 ms) restituiscono la stessa sequenza. Questo è documentato e "by-design". Ciò è particolarmente fastidioso se si desidera eseguire il multi-thread del codice, poiché non è possibile creare semplicemente un'istanza di Randomall'inizio dell'esecuzione di ogni thread.

La soluzione alternativa consiste nel prestare la massima attenzione quando si utilizza il costruttore predefinito e eseguire manualmente il seed quando necessario.

Un altro problema qui è che lo spazio seed è piuttosto piccolo (31 bit). Quindi, se generi 50k istanze di Randomcon semi perfettamente casuali, probabilmente otterrai due volte una sequenza di numeri casuali (a causa del paradosso del compleanno ). Quindi anche la semina manuale non è facile da ottenere.

Difetto 2: la distribuzione dei numeri casuali restituiti da Next(int maxValue)è parziale

Ci sono parametri per i quali Next(int maxValue)chiaramente non è uniforme. Ad esempio se calcoli r.Next(1431655765) % 2otterrai 0circa 2/3 dei campioni. (Codice di esempio alla fine della risposta.)

Difetto 3: il NextBytes()metodo è inefficiente.

Il costo per byte di NextBytes()è grande circa quanto il costo per generare un campione intero completo con Next(). Da questo sospetto che creino effettivamente un campione per byte.

Una migliore implementazione utilizzando 3 byte di ogni campione accelererebbe NextBytes()di quasi un fattore 3.

Grazie a questo difetto Random.NextBytes()è solo circa il 25% più veloce rispetto System.Security.Cryptography.RNGCryptoServiceProvider.GetBytesalla mia macchina (Win7, Core i3 2600MHz).

Sono sicuro che se qualcuno ispezionasse il codice sorgente / byte decompilato troverebbe ancora più difetti di quelli che ho trovato con la mia analisi della scatola nera.


Esempi di codice

r.Next(0x55555555) % 2 è fortemente prevenuto:

Random r = new Random();
const int mod = 2;
int[] hist = new int[mod];
for(int i = 0; i < 10000000; i++)
{
    int num = r.Next(0x55555555);
    int num2 = num % 2;
    hist[num2]++;
}
for(int i=0;i<mod;i++)
    Console.WriteLine(hist[i]);

Prestazione:

byte[] bytes=new byte[8*1024];
var cr=new System.Security.Cryptography.RNGCryptoServiceProvider();
Random r=new Random();

// Random.NextBytes
for(int i=0;i<100000;i++)
{
    r.NextBytes(bytes);
}

//One sample per byte
for(int i=0;i<100000;i++)
{   
    for(int j=0;j<bytes.Length;j++)
      bytes[j]=(byte)r.Next();
}

//One sample per 3 bytes
for(int i=0;i<100000;i++)
{
    for(int j=0;j+2<bytes.Length;j+=3)
    {
        int num=r.Next();
        bytes[j+2]=(byte)(num>>16);   
        bytes[j+1]=(byte)(num>>8);
        bytes[j]=(byte)num;
    }
    //Yes I know I'm not handling the last few bytes, but that won't have a noticeable impact on performance
}

//Crypto
for(int i=0;i<100000;i++)
{
    cr.GetBytes(bytes);
}

1
Interessante, posso confermare la tua scoperta: sulla mia macchina Next (1431655765) dà anche 2/3 con eventuale semina. Qual è la magia di 1431655765? Come sei arrivato a questo numero?
citykid

1
@citykid Guarda il numero come esadecimale o bit. La sua magia nasce dal dubbio modo Randomdi trasformare un intero a 31 bit in un numero con il limite superiore specificato. Ho dimenticato i dettagli, ma è qualcosa di simile randomValue * max / 2^{31}.
CodesInChaos

1431655765_10 = 1010101010101010101010101010101_2
Tim S.

6
Hm. Quindi quale implementazione di Random per C # consigliate di utilizzare?
Arsen Zahray

1
Holy cow, la non uniformità della distribuzione di Next(), dimostrata da te qui, è un bug piuttosto spettacolare - e ancora presente oggi, 6 anni dopo che hai scritto per la prima volta le tue scoperte. (Dico "bug" piuttosto che semplicemente "difetto", perché i documenti affermano che "i numeri pseudo-casuali sono scelti con uguale probabilità da un insieme finito di numeri" . Non è così, e il tuo codice qui lo dimostra.)
Mark Amery

24

System.Random è molto più performante poiché non genera numeri casuali crittograficamente sicuri.

Un semplice test sulla mia macchina che riempie un buffer di 4 byte con dati casuali 1.000.000 di volte richiede 49 ms per Random, ma 2845 ms per RNGCryptoServiceProvider. Nota che se aumenti la dimensione del buffer che stai riempiendo, la differenza si restringe poiché l'overhead per RNGCryptoServiceProvider è meno rilevante.


2
Grazie per averlo dimostrato con un test effettivo.
Lernkurve

3
Potresti pensare che sia duro, ma -1 per pubblicare i risultati di un benchmark delle prestazioni senza includere il codice del benchmark. Anche se le caratteristiche delle prestazioni Randome RNGCryptoServiceProvidernon sono cambiate negli ultimi 8 anni (cosa che per quanto ne so potrebbero aver), ho visto abbastanza benchmark completamente non funzionanti usati su Stack Overflow per non fidarsi dei risultati di un benchmark il cui codice non è disponibile pubblicamente.
Mark Amery

21

Le ragioni più ovvie sono già state menzionate, quindi eccone una più oscura: i PRNG crittografici in genere devono essere continuamente riseminati con entropia "reale". Pertanto, se si utilizza un CPRNG troppo spesso, si potrebbe esaurire il pool di entropia del sistema, che (a seconda dell'implementazione del CPRNG) lo indebolirà (consentendo così a un attaccante di prevederlo) o si bloccherà durante il tentativo di riempirsi il suo pool di entropia (diventando così un vettore di attacco per un attacco DoS).

In ogni caso, la tua applicazione è ora diventata un vettore di attacco per altre applicazioni totalmente indipendenti che, a differenza della tua, in realtà dipendono in modo vitale dalle proprietà crittografiche del CPRNG.

Questo è un vero problema del mondo reale, BTW, che è stato osservato su server headless (che naturalmente hanno pool di entropia piuttosto piccoli perché mancano di fonti di entropia come input da mouse e tastiera) che eseguono Linux, dove le applicazioni usano erroneamente il /dev/randomkernel CPRNG per tutti i tipi di numeri casuali, mentre il comportamento corretto sarebbe leggere un piccolo valore seed da /dev/urandome usarlo per creare il proprio PRNG.


Ho letto l'articolo di Wikipedia e alcune altre fonti Internet sull'entropia e l'esaurimento dell'entropia, e non lo capisco del tutto. Come posso esaurire il pool di entropia quando il generatore di numeri casuali viene alimentato con l'ora di sistema, il numero di byte liberi ecc.? Come possono gli altri utilizzarlo come vettore di attacco per prevedere numeri casuali? Puoi fornire un semplice esempio? Forse questa discussione deve essere portata offline. en.wikipedia.org/wiki/Entropy_%28computing%29
Lernkurve

3
L'ora di sistema non è una fonte di entropia, perché è prevedibile. Non sono sicuro del numero di byte liberi, ma dubito che sia una fonte di entropia di alta qualità. Inviando più richieste al server, l'attaccante può far diminuire il numero di byte liberi, rendendolo parzialmente deterministico. L'applicazione diventa un vettore di attacco perché esaurendo il pool di entropia, costringe l'altra applicazione critica per la sicurezza a utilizzare numeri casuali meno casuali o ad attendere fino a quando la fonte di entropia non viene reintegrata.
quant_dev

Capisco che se si dispone di un generatore pseudo-casuale alimentato, ad esempio, con un seme a 32 bit, un attacco a forza bruta sarà spesso abbastanza facile; anche un seme a 64 bit può essere soggetto ad attacchi di compleanno. Una volta che il seme diventa molto più grande di quello, però, non vedo il rischio. Se si dispone di un generatore casuale che per ogni byte di output prende passa uno stato a 128 bit attraverso un algoritmo di crittografia a blocchi e quindi emette gli 8 bit inferiori, come potrebbe un attaccante anche con gig di byte di output consecutivi dedurre lo stato, debolezze assenti in l'algoritmo di crittografia stesso?
supercat

11

Se stai programmando un gioco di carte o una lotteria online, assicurati che la sequenza sia quasi impossibile da indovinare. Tuttavia, se mostri agli utenti, ad esempio, una citazione del giorno in cui le prestazioni sono più importanti della sicurezza.


9

Questo è stato discusso a lungo, ma alla fine, la questione delle prestazioni è una considerazione secondaria quando si seleziona un RNG. Esiste una vasta gamma di RNG là fuori e il Lehmer LCG in scatola di cui è composta la maggior parte degli RNG di sistema non è il migliore né necessariamente il più veloce. Su sistemi vecchi e lenti era un ottimo compromesso. Quel compromesso è raramente davvero rilevante in questi giorni. La cosa persiste nei sistemi odierni principalmente perché A) la cosa è già costruita, e non c'è una vera ragione per `` reinventare la ruota '' in questo caso, e B) per ciò per cui la maggior parte delle persone la userà, è 'abbastanza buono'.

In definitiva, la selezione di un RNG si riduce al rapporto rischio / rendimento. In alcune applicazioni, ad esempio un videogioco, non c'è alcun rischio. Un Lehmer RNG è più che adeguato ed è piccolo, conciso, veloce, ben compreso e "nella scatola".

Se l'applicazione è, ad esempio, un gioco di poker o una lotteria online in cui sono coinvolti premi effettivi e denaro reale entra in gioco a un certo punto dell'equazione, il Lehmer "in the box" non è più adeguato. In una versione a 32 bit, ha solo 2 ^ 32 possibili stati validi prima che inizi a funzionare al meglio . In questi giorni, questa è una porta aperta a un attacco di forza bruta. In un caso come questo, lo sviluppatore vorrà andare a qualcosa come un RNG di periodo molto lungo di alcune specie e probabilmente iniziarlo da un provider crittograficamente forte. Questo offre un buon compromesso tra velocità e sicurezza. In tal caso, la persona andrà alla ricerca di qualcosa come il Mersenne Twister , o un generatore ricorsivo multiplo di qualche tipo.

Se l'applicazione è qualcosa come la comunicazione di grandi quantità di informazioni finanziarie su una rete, ora c'è un rischio enorme e supera di gran lunga qualsiasi possibile ricompensa. Ci sono ancora auto blindate perché a volte gli uomini pesantemente armati sono l'unica sicurezza adeguata, e credimi, se una brigata di agenti speciali con carri armati, combattenti ed elicotteri fosse finanziariamente fattibile, sarebbe il metodo di scelta. In un caso come questo, ha senso utilizzare un RNG crittograficamente potente, perché qualunque sia il livello di sicurezza che puoi ottenere, non è quanto vuoi. Quindi prenderai tutto ciò che puoi trovare e il costo è un problema di secondo posto molto, molto remoto, in termini di tempo o denaro. E se questo significa che ogni sequenza casuale impiega 3 secondi per essere generata su un computer molto potente, dovrai aspettare i 3 secondi,


3
Penso che ti sbagli sulle tue magnitudini; l'invio di dati finanziari deve essere estremamente veloce; se il tuo algoritmo di trading può arrivare al risultato 0,1 ms più velocemente della concorrenza, finirai meglio nella coda dei comandi di acquisto / vendita / stop-loss / quotazione. 3 secondi sono un'eternità. Questo è il motivo per cui i trader investono in computer incredibilmente buoni. Vedi la risposta precedente; Crypt.RNG richiede solo 0,0028 ms per nuovo numero; 0,0000028 secondi, quindi sei fuori di 9 ordini di grandezza in termini di quantità di elaborazione necessaria e anche di quanto sia importante la velocità.
Henrik


4

Non tutti hanno bisogno di numeri casuali crittograficamente sicuri e potrebbero trarre maggiori vantaggi da una semplice prng più veloce. Forse la cosa più importante è che puoi controllare la sequenza per i numeri System.Random.

In una simulazione che utilizza numeri casuali che potresti voler ricreare, riesegui la simulazione con lo stesso seme. Può essere utile per tenere traccia dei bug quando si desidera rigenerare anche un determinato scenario difettoso, eseguendo il programma con la stessa identica sequenza di numeri casuali che ha causato il crash del programma.


2

Se non ho bisogno della sicurezza, cioè, voglio solo un valore relativamente indeterminato non uno che sia crittograficamente forte, Random ha un'interfaccia molto più semplice da usare.


2

Esigenze diverse richiedono RNG diversi. Per la crittografia, vuoi che i tuoi numeri casuali siano il più casuali possibile. Per le simulazioni Monte Carlo, si desidera che riempiano lo spazio in modo uniforme e che siano in grado di avviare l'RNG da uno stato noto.


1
Se solo System.Random lo avesse fatto ... oh, beh.
user2864740

2

Random non è un generatore di numeri casuali, è un generatore di sequenze pseudo-casuali deterministiche, che prende il nome per ragioni storiche.

Il motivo per utilizzarlo System.Randomè se si desidera queste proprietà, vale a dire una sequenza deterministica, che garantisce la produzione della stessa sequenza di risultati quando inizializzata con lo stesso seme.

Se vuoi migliorare la "casualità" senza sacrificare l'interfaccia, puoi ereditare System.Randomsovrascrivendo diversi metodi.

Perché vorresti una sequenza deterministica

Un motivo per avere una sequenza deterministica piuttosto che una vera casualità è perché è ripetibile.

Ad esempio, se si esegue una simulazione numerica, è possibile inizializzare la sequenza con un numero casuale (vero) e registrare quale numero è stato utilizzato .

Quindi, se desideri ripetere la stessa identica simulazione, ad esempio per scopi di debug, puoi farlo inizializzando invece la sequenza con il valore registrato .

Perché dovresti volere questa sequenza particolare, non molto buona

L'unico motivo a cui riesco a pensare sarebbe per la retrocompatibilità con il codice esistente che utilizza questa classe.

In breve, se vuoi migliorare la sequenza senza cambiare il resto del tuo codice, vai avanti.


1

Ho scritto un gioco (Crystal Sliders su iPhone: qui ) che avrebbe messo una serie "casuale" di gemme (immagini) sulla mappa e avresti ruotato la mappa come volevi, selezionandole e se ne andavano. - Simile a Bejeweled. Stavo usando Random (), ed è stato seminato con il numero di tick 100ns dall'avvio del telefono, un seme piuttosto casuale.

Ho trovato incredibile che avrebbe generato giochi quasi identici tra loro: delle 90 gemme circa, di 2 colori, avrei ottenuto due ESATTAMENTE uguali tranne da 1 a 3 gemme! Se lanci 90 monete e ottieni lo stesso schema tranne che per 1-3 lanci, è MOLTO improbabile! Ho diverse schermate che mostrano loro lo stesso. Sono rimasto scioccato da quanto fosse cattivo System.Random ()! Ho pensato che DEVO aver scritto qualcosa di terribilmente sbagliato nel mio codice e che lo stavo usando in modo sbagliato. Mi sbagliavo però, era il generatore.

Come esperimento - e come soluzione finale, sono tornato al generatore di numeri casuali che utilizzo dal 1985 o giù di lì - che è MOLTO meglio. È più veloce, ha un periodo di 1,3 * 10 ^ 154 (2 ^ 521) prima che si ripeta. L'algoritmo originale è stato seminato con un numero a 16 bit, ma l'ho cambiato in un numero a 32 bit e ho migliorato il seeding iniziale.

Quello originale è qui:

ftp://ftp.grnet.gr/pub/lang/algorithms/c/jpl-c/random.c

Nel corso degli anni, ho eseguito tutti i test con numeri casuali a cui riuscivo a pensare, e li ho superati tutti. Non mi aspetto che abbia alcun valore come crittografia, ma restituisce un numero veloce come "return * p ++;" finché non esaurisce i 521 bit, quindi esegue un rapido processo sui bit per crearne di nuovi casuali.

Ho creato un wrapper C # - chiamato JPLRandom () ha implementato la stessa interfaccia di Random () e ha cambiato tutti i punti in cui l'ho chiamato nel codice.

La differenza era ESTREMAMENTE migliore - OMG sono rimasto sbalordito - non dovrebbe esserci alcun modo di poterlo dire guardando gli schermi di circa 90 gemme in uno schema, ma ho fatto una versione di emergenza del mio gioco dopo questo.

E non userei mai più System.Random () per niente. Sono SCOLORATO che la loro versione sia stata spazzata via da qualcosa che ora ha 30 anni!

-Traderhut Games


3
La mia prima ipotesi è che tu abbia ricreato Randomtroppo spesso. Dovrebbe essere creato solo una volta chiamando Nextquell'istanza molte volte. Randomè brutto, ma non così male. Puoi pubblicare un programma di esempio insieme a un paio di semi che presentano questo problema?
CodesInChaos

Il codice creerebbe un Random () all'inizio di ogni livello (ma era un grosso problema con il livello 1 più di quelli successivi) Il codice era più o meno il seguente:
Traderhut Games

Rnd = nuovo Random ((uint) GameSeed); NextGameSeed = Rnd.Next (2000000000); Ogni livello utilizzava un nuovo Random creato con un nuovo seed - Il Seed è stato salvato per ogni livello in modo da poter ricreare la mappa e anche confermare la sequenza di seed casuali abbinati. Questo mi permette di confermare che il gioco è una serie valida di mappe che sono state risolte e di ricreare il gioco.
Traderhut Games

Inizialmente, Random è stato creato in base a System.DateTime.Now.Ticks (o 0), quindi il GameSeed è stato scelto utilizzando la stessa chiamata di Rnd.Next () sopra. Se non riesco a farlo, allora c'è un problema serio con il seeding del generatore di numeri casuali.
Giochi Traderhut

questa non è una risposta alla domanda originale!
Mike Dinescu
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.