Por que usar a classe C # System.Random em vez de System.Security.Cryptography.RandomNumberGenerator?

86

Por que alguém usaria o gerador de número aleatório "padrão" de System.Random em vez de sempre usar o gerador de número aleatório criptograficamente seguro de System.Security.Cryptography.RandomNumberGenerator (ou suas subclasses porque RandomNumberGenerator é abstrato)?

Nate Lawson nos diz em sua apresentação do Google Tech Talk " Crypto Strikes Back " no minuto 13:11 para não usar os geradores de número aleatório "padrão" de Python, Java e C # e, em vez disso, usar a versão criptograficamente segura.

Eu sei a diferença entre as duas versões de geradores de números aleatórios (consulte a pergunta 101337 ).

Mas que razão existe para nem sempre usar o gerador de números aleatórios seguro? Por que usar System.Random? Desempenho, talvez?

Lernkurve
fonte
7
Qual você prefere digitar?
Macha de
13
Muitas pessoas usam isso seriamente como uma justificativa para o que fazem (geralmente não em voz alta). O código é mais lido do que escrito, quem se importa com as diferenças triviais de comprimento?
Mark Sowul
3
Mas, de qualquer forma, por que você deveria usar RNGs criptográficos se você não está fazendo criptografia?
Mark Sowul
3
@Macha, é para isso que servem os apelidos ->using R = System.Security.Cryptography.RandomNumberGenerator; R.Create();
cchamberlain

Respostas:

146

Velocidade e intenção. Se você está gerando um número aleatório e não precisa de segurança, por que usar uma função de criptografia lenta? Você não precisa de segurança, então por que fazer outra pessoa pensar que o número pode ser usado para algo seguro quando não será?

Kevin LaBranche
fonte
31
Gosto muito do argumento da intenção.
Lernkurve de
12
Deve-se notar que Random.GetNext está longe de ser bom em "espalhar" os números aleatórios pelo espectro, especialmente em um ambiente encadeado. Encontrei esse problema ao escrever um programa para testar diferentes soluções para o problema Rand7 de Rand5. Em um teste encadeado rápido agora de 100.000 números aleatórios entre 0 e 10, 82470 dos números gerados foram 0. Eu vi discrepâncias semelhantes em meus testes anteriores. A criptografia aleatória é muito uniforme na distribuição dos números. Acho que a lição é sempre testar seus dados aleatórios para ver se são "aleatórios o suficiente" para suas necessidades.
Kristoffer L
35
@Kristoffer Acho que você usou mal Random. Deixe-me adivinhar: você criou uma nova instância da Randomclasse para cada número, que, como é propagada por um temporizador aproximado, será propagada com o mesmo valor por um intervalo de cerca de 1-16 ms.
CodesInChaos
15
@CodesInChaos: Além disso, existe uma condição de corrida Randomque faz com que ele retorne todos os 0's quando o mesmo objeto é usado por vários threads.
BlueRaja - Danny Pflughoeft
3
@KristofferL: Veja o comentário acima, veja também esta resposta
BlueRaja - Danny Pflughoeft
65

Além da velocidade e da interface mais útil ( NextDouble()etc), também é possível fazer uma sequência aleatória repetível usando um valor de semente fixo. Isso é bastante útil, entre outros, durante o teste.

Random gen1 = new Random();     // auto seeded by the clock
Random gen2 = new Random(0);    // Next(10) always yields 7,8,7,5,2,....
Henk Holterman
fonte
2
E há o BitConverter.ToInt32 (valor Byte [], int startIndex) que pode ser mais fácil de entender. ;)
sisve
7
Ian Bell e David Braben usaram um gerador aleatório no jogo de computador Elite para criar uma vasta lista de planetas e seus atributos (tamanho, etc), com memória muito limitada. Isso também depende do gerador criar um padrão determinístico (de uma semente) - que o Crypto obviamente não fornece (por design). Há mais algumas informações sobre como eles fizeram isso aqui: wiki.alioth.net/index.php / Random_number_generator e o livro "Infinite Game Universe: Mathematical Techniques" ISBN: 1584500581 tem uma discussão mais geral sobre tais técnicas.
Daniel James Bryars
7
Lembre-se de que o MSDN não garante que essa propriedade seja mantida em todas as versões do .NET: "A implementação do gerador de número aleatório na classe Random não tem garantia de permanecer a mesma nas versões principais do .NET Framework."
Roman Starkov
2
@phoog "Como resultado, o código do seu aplicativo não deve presumir que a mesma semente resultará na mesma sequência pseudo-aleatória em diferentes versões do .NET Framework." - Não sei, parece bem claro para mim. No entanto, eu não ficaria surpreso se eles não pudessem mudar isso na prática sem quebrar os programas existentes, apesar desse aviso.
Roman Starkov,
2
@phoog: Você está dizendo uma coisa e então o oposto disso. Você está se contradizendo diretamente.
Timwi,
53

Em primeiro lugar, a apresentação que você vinculou fala apenas sobre números aleatórios para fins de segurança. Portanto, ele não afirma Randomser ruim para fins não relacionados à segurança.

Mas eu afirmo que sim. A implementação do .net 4 Randomapresenta falhas de várias maneiras. Eu recomendo usá-lo apenas se você não se importar com a qualidade de seus números aleatórios. Eu recomendo usar melhores implementações de terceiros.

Falha 1: a semeadura

O construtor padrão é propagado com a hora atual. Assim, todas as instâncias Randomcriadas com o construtor padrão em um curto período de tempo (ca. 10ms) retornam a mesma sequência. Isso é documentado e "por design". Isso é particularmente irritante se você quiser multi-thread seu código, já que você não pode simplesmente criar uma instância de Randomno início de cada execução de thread.

A solução alternativa é ser extremamente cuidadoso ao usar o construtor padrão e propagar manualmente quando necessário.

Outro problema aqui é que o espaço da semente é bastante pequeno (31 bits). Portanto, se você gerar 50k instâncias de Randomcom sementes perfeitamente aleatórias, provavelmente obterá uma sequência de números aleatórios duas vezes (devido ao paradoxo do aniversário ). Portanto, a semeadura manual também não é fácil de acertar.

Falha 2: A distribuição de números aleatórios retornados por Next(int maxValue)é tendenciosa

Existem parâmetros para os quais Next(int maxValue)claramente não são uniformes. Por exemplo, se você calcular r.Next(1431655765) % 2, obterá 0cerca de 2/3 das amostras. (Amostra de código no final da resposta.)

Falha 3: o NextBytes()método é ineficiente.

O custo por byte de NextBytes()é quase tão grande quanto o custo para gerar uma amostra inteira inteira com Next(). A partir disso, suspeito que eles realmente criam uma amostra por byte.

Uma implementação melhor usando 3 bytes de cada amostra seria acelerada NextBytes()em quase um fator 3.

Graças a esta falha, Random.NextBytes()é apenas cerca de 25% mais rápido do que System.Security.Cryptography.RNGCryptoServiceProvider.GetBytesna minha máquina (Win7, Core i3 2600MHz).

Tenho certeza de que se alguém inspecionasse o código de byte de origem / descompilado, encontraria ainda mais falhas do que eu encontrei com minha análise de caixa preta.


Amostras de código

r.Next(0x55555555) % 2 é fortemente tendencioso:

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

Atuação:

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);
}
CodesInChaos
fonte
1
Interessante, pode confirmar sua descoberta: na minha máquina Avançar (1431655765) também dá 2/3 com qualquer semeadura. Qual é a magia de 1431655765? Como você chegou a esse número?
citykid
1
@citykid Veja o número como hexadecimal ou bits. Sua mágica surge da maneira duvidosa que Randomusa para transformar um inteiro de 31 bits em um número com o limite superior especificado. Esqueci os detalhes, mas é algo parecido randomValue * max / 2^{31}.
CodesInChaos
1431655765_10 = 1010101010101010101010101010101_2
Tim S.
6
Hm. Então, qual implementação de Random para C # você recomenda usar?
Arsen Zahray
1
Caramba, a não uniformidade da distribuição de Next(), demonstrada por você aqui, é um bug bastante espetacular - e ainda está presente hoje, 6 anos após você ter escrito suas descobertas. (Eu digo "bug" em vez de apenas "falha", porque os documentos afirmam que "Números pseudo-aleatórios são escolhidos com probabilidade igual de um conjunto finito de números" . Não é assim, e seu código aqui prova isso.)
Mark Amery
24

System.Random tem muito mais desempenho, pois não gera números aleatórios criptograficamente seguros.

Um teste simples em minha máquina preenchendo um buffer de 4 bytes com dados aleatórios 1.000.000 vezes leva 49 ms para Random, mas 2.845 ms para RNGCryptoServiceProvider. Observe que, se você aumentar o tamanho do buffer que está enchendo, a diferença diminui, pois a sobrecarga para RNGCryptoServiceProvider é menos relevante.

Michael
fonte
2
Obrigado por demonstrar isso com um teste real.
Lernkurve de
3
Você pode achar que isso é duro, mas -1 para postar resultados de um benchmark de desempenho sem incluir o código do benchmark. Mesmo que as características de desempenho Randome RNGCryptoServiceProvidernão tenham mudado nos últimos 8 anos (o que, pelo que sei, podem ter mudado), já vi benchmarks completamente quebrados o suficiente usados ​​no Stack Overflow para não confiar nos resultados de um benchmark cujo código não está publicamente disponível.
Mark Amery
21

As razões mais óbvias já foram mencionadas, então aqui está uma mais obscura: PRNGs criptográficos normalmente precisam ser continuamente propagados novamente com entropia "real". Assim, se você usar um CPRNG com muita frequência, poderá esgotar o pool de entropia do sistema, o que (dependendo da implementação do CPRNG) o enfraquecerá (permitindo que um invasor o preveja) ou bloqueará enquanto tenta preencher seu pool de entropia (tornando-se assim um vetor de ataque para um ataque DoS).

De qualquer forma, seu aplicativo agora se tornou um vetor de ataque para outros aplicativos totalmente não relacionados que - ao contrário do seu - na verdade dependem vitalmente das propriedades criptográficas do CPRNG.

Este é um problema real do mundo real, BTW, que foi observado em servidores headless (que naturalmente têm pools de entropia bastante pequenos porque não possuem fontes de entropia como entrada de mouse e teclado) executando Linux, onde os aplicativos usam incorretamente o /dev/randomkernel CPRNG para todos os tipos de números aleatórios, enquanto o comportamento correto seria ler um pequeno valor de semente /dev/urandome usá-lo para semear seu próprio PRNG.

Jörg W Mittag
fonte
Eu li o artigo da Wikipedia e algumas outras fontes da Internet sobre entropia e depleção de entropia, e não entendo muito bem. Como posso esgotar o pool de entropia quando o gerador de número aleatório é alimentado com o tempo do sistema, número de bytes livres, etc.? Como outras pessoas podem usá-lo como vetor de ataque para prever números aleatórios? Você pode fornecer um exemplo fácil? Talvez esta discussão deva ser desligada. en.wikipedia.org/wiki/Entropy_%28computing%29
Lernkurve
3
O tempo do sistema não é uma fonte de entropia, porque é previsível. Não tenho certeza sobre o número de bytes livres, mas duvido que seja uma fonte de entropia de alta qualidade também. Ao enviar mais solicitações ao servidor, o invasor pode fazer com que o número de bytes livres diminua, tornando-o parcialmente determinístico. Seu aplicativo se torna um vetor de ataque porque, ao esgotar o pool de entropia, força o outro aplicativo de segurança crítica a usar números aleatórios menos aleatórios - ou esperar até que a fonte de entropia seja reabastecida.
quant_dev
Eu entendo que se alguém tiver um gerador pseudo-aleatório alimentado, por exemplo, com uma semente de 32 bits, um ataque de força bruta geralmente será bastante fácil; mesmo uma semente de 64 bits pode estar sujeita a ataques de aniversário. Uma vez que a semente fica muito maior do que isso, porém, não vejo bem o risco. Se alguém tiver um gerador aleatório que para cada byte de saída passa um estado de 128 bits por meio de um algoritmo de criptografia de bloco e, em seguida, produz os 8 bits inferiores, como poderia um invasor, mesmo com gigs de bytes de saída consecutivos, inferir o estado, sem fraquezas em o algoritmo de criptografia em si?
supercat de
11

Se estiver programando um jogo de cartas ou loteria online, você deve ter certeza de que a sequência é quase impossível de adivinhar. No entanto, se você estiver mostrando aos usuários, digamos, uma citação do dia, o desempenho é mais importante do que a segurança.

Dan Diplo
fonte
9

Isso foi discutido longamente, mas, em última análise, a questão do desempenho é uma consideração secundária ao selecionar um RNG. Há uma vasta gama de RNGs por aí, e o Lehmer LCG enlatado no qual a maioria dos RNGs de sistema consiste não é o melhor nem necessariamente o mais rápido. Em sistemas antigos e lentos, era um excelente compromisso. Esse compromisso raramente é realmente relevante nos dias de hoje. A coisa persiste nos sistemas atuais principalmente porque A) a coisa já está construída, e não há nenhuma razão real para 'reinventar a roda' neste caso, e B) para o que a grande maioria das pessoas a usará, é 'bom o bastante'.

Em última análise, a seleção de um RNG se resume à relação Risco / Recompensa. Em algumas aplicações, por exemplo em um videogame, não existe nenhum risco. Um Lehmer RNG é mais do que adequado e é pequeno, conciso, rápido, bem compreendido e "pronto para uso".

Se o aplicativo for, por exemplo, um jogo de pôquer on-line ou loteria onde há prêmios reais envolvidos e dinheiro real entra em jogo em algum ponto da equação, o Lehmer 'na caixa' não é mais adequado. Em uma versão de 32 bits, ele tem apenas 2 ^ 32 estados válidos possíveis antes de começar a circular, na melhor das hipóteses . Hoje em dia, essa é uma porta aberta para um ataque de força bruta. Em um caso como esse, o desenvolvedor vai querer ir para algo como um RNG de Período Muito Longo de algumas espécies e provavelmente semeá-lo de um provedor criptograficamente forte. Isso dá um bom compromisso entre velocidade e segurança. Nesse caso, a pessoa sairá procurando por algo como o Mersenne Twister ou um gerador recursivo múltiplo de algum tipo.

Se o aplicativo é algo como comunicar grandes quantidades de informações financeiras em uma rede, agora existe um risco enorme e supera qualquer recompensa possível. Ainda há carros blindados porque às vezes homens fortemente armados são a única segurança adequada e, acredite, se uma brigada de pessoal de operações especiais com tanques, caças e helicópteros fosse financeiramente viável, esse seria o método escolhido. Em um caso como este, usar um RNG criptograficamente forte faz sentido, porque qualquer nível de segurança que você pode obter, não é o tanto que você deseja. Portanto, você pegará o máximo que puder encontrar e o custo é uma questão secundária muito, muito remota, seja em tempo ou dinheiro. E se isso significa que cada sequência aleatória leva 3 segundos para ser gerada em um computador muito poderoso, você vai esperar os 3 segundos,


fonte
3
Acho que você está errado sobre suas magnitudes; o envio de dados financeiros precisa ser extremamente rápido; se o seu algoritmo de negociação puder chegar ao resultado 0,1 ms mais rápido do que a concorrência, você terminará melhor na fila de comandos de compra / venda / stop-loss / quote. 3 segundos é uma eternidade. É por isso que os comerciantes investem em computadores incrivelmente bons. Veja a resposta anterior; Crypt.RNG leva apenas 0,0028 ms por novo número; 0,0000028 segundos, então você está errado em 9 ordens de magnitude em termos de quanto processamento é necessário e também na importância da velocidade.
Henrik
4

Nem todo mundo precisa de números aleatórios criptograficamente seguros e podem se beneficiar mais de uma impressão simples mais rápida. Talvez o mais importante seja que você pode controlar a sequência de números System.Random.

Em uma simulação utilizando números aleatórios que você pode querer recriar, você executa novamente a simulação com a mesma semente. Pode ser útil para rastrear bugs quando você deseja regenerar um dado cenário com falha também - executando seu programa com a mesma sequência exata de números aleatórios que travou o programa.

nos
fonte
2

Se eu não precisar da segurança, ou seja, só quero um valor relativamente indeterminado, não um criptograficamente forte, o Random tem uma interface muito mais fácil de usar.

Tvanfosson
fonte
2

Diferentes necessidades exigem diferentes RNGs. Para criptografia, você deseja que seus números aleatórios sejam o mais aleatórios possível. Para simulações de Monte Carlo, você deseja que preencham o espaço uniformemente e sejam capazes de iniciar o RNG a partir de um estado conhecido.

quant_dev
fonte
1
Se ao menos System.Random fizesse algum ... bem.
user2864740
2

Random não é um gerador de números aleatórios, é um gerador de sequência pseudo-aleatória determinística, que leva seu nome por razões históricas.

A razão para usar System.Randomé se você deseja essas propriedades, ou seja, uma sequência determinística, que é garantida para produzir a mesma sequência de resultados quando inicializada com a mesma semente.

Se você quiser melhorar a "aleatoriedade" sem sacrificar a interface, pode herdar da System.Randomsubstituição de vários métodos.

Por que você quer uma sequência determinística

Uma razão para ter uma sequência determinística em vez da verdadeira aleatoriedade é porque ela pode ser repetida.

Por exemplo, se você estiver executando uma simulação numérica, pode inicializar a sequência com um número aleatório (verdadeiro) e registrar o número usado .

Então, se você deseja repetir exatamente a mesma simulação, por exemplo, para fins de depuração, você pode fazer isso inicializando a sequência com o valor registrado .

Por que você iria querer esta sequência particular, não muito boa,

A única razão que consigo pensar seria a compatibilidade com versões anteriores do código existente que usa essa classe.

Resumindo, se você quiser melhorar a sequência sem alterar o resto do código, vá em frente.

Ben
fonte
1

Eu escrevi um jogo (Crystal Sliders no iPhone: Aqui ) que colocaria uma série "aleatória" de joias (imagens) no mapa e você giraria o mapa como queria e as selecionaria e elas iriam embora. - Semelhante ao Bejeweled. Eu estava usando Random (), e foi semeado com o número de tiques de 100 ns desde que o telefone foi inicializado, uma semente bastante aleatória.

Achei incrível que ele gerasse jogos quase idênticos entre si - das 90 ou mais joias, de 2 cores, eu obteria duas EXATAMENTE iguais, exceto por 1 a 3 joias! Se você lançar 90 moedas e obter o mesmo padrão, exceto por 1-3, isso é MUITO improvável! Tenho várias capturas de tela que mostram o mesmo. Fiquei chocado com o quão ruim System.Random () era! Eu presumi que DEVERIA ter escrito algo terrivelmente errado em meu código e estava usando-o errado. Eu estava errado, era o gerador.

Como um experimento - e uma solução final, voltei ao gerador de números aleatórios que uso desde 1985 ou mais - que é VASTMENTE melhor. É mais rápido, tem um período de 1,3 * 10 ^ 154 (2 ^ 521) antes de se repetir. O algoritmo original foi propagado com um número de 16 bits, mas eu mudei para um número de 32 bits e melhorei a propagação inicial.

O original está aqui:

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

Com o passar dos anos, fiz todos os testes de números aleatórios em que pude pensar e superei todos eles. Não espero que tenha algum valor criptográfico, mas retorna um número tão rápido quanto "return * p ++;" até esgotar os 521 bits e, em seguida, executar um processo rápido sobre os bits para criar novos bits aleatórios.

Criei um wrapper C # - chamei-o de JPLRandom (), implementou a mesma interface que Random () e alterei todos os lugares onde o chamei no código.

A diferença era MUITO melhor - OMG, fiquei pasmo - não deveria haver nenhuma maneira que eu pudesse dizer apenas olhando para as telas de 90 ou mais joias em um padrão, mas fiz um lançamento de emergência do meu jogo seguindo isso.

E eu nunca usaria System.Random () para nada novamente. Estou CHOCADO que a versão deles tenha se surpreendido com algo que agora tem 30 anos!

Jogos -Traderhut

Jogos Traderhut
fonte
3
Meu primeiro palpite é que você recriou com Randommuita frequência. Ele deve ser criado apenas uma vez, chamando Nextessa instância várias vezes. Randomé ruim, mas não tão ruim assim. Você pode postar um programa de amostra junto com um par de sementes que exiba esse problema?
CodesInChaos
O código criaria um Random () no início de cada nível (mas era um grande problema com o nível 1 mais do que com os posteriores). O código era aproximadamente o seguinte:
Traderhut Games
Rnd = novo Aleatório ((uint) GameSeed); NextGameSeed = Rnd.Next (2000000000); Cada nível usou um novo Aleatório que foi criado com uma nova semente - A semente foi salva para cada nível para que eu pudesse recriar o mapa e também confirmar a sequência de sementes aleatórias correspondentes. Isso me permite confirmar que o jogo é uma série válida de mapas que foram resolvidos e recriar o jogo.
Traderhut Games
E inicialmente, Random foi criado com base em System.DateTime.Now.Ticks (ou 0), e então o GameSeed foi escolhido usando a mesma chamada de Rnd.Next () acima. Se eu não puder fazer isso, há um problema sério com a propagação do gerador de números aleatórios.
Traderhut Games
esta não é uma resposta à pergunta original!
Mike Dinescu,