• Nenhum resultado encontrado

DESCRIÇÃO DA IMPLEMENTAÇÃO

No documento Quebra-Cabeças Criptográficos (páginas 71-86)

Esta seção será dividida da seguinte maneira: para cada uma das três classes, divididas entre os três pacotes, será descrito, no mínimo, o método de inicialização (que recebe parâmetros e carrega algumas variáveis indispensáveis para o algoritmo), o método que cria o quebra- cabeça, e o que resolve o mesmo. Os testes produzidos para cada classe deram origem aos resultados experimentais, que serão expressos no Ca- pítulo 7.

6.2.1 Time Lock

A primeira classe implementada foi a do algoritmo TimeLock. O primeiro método que deve ser chamado, antes mesmo de criar o quebra-cabeça, é o método get_s, que vai calcular quantas operações de quadrado modular a máquina consegue fazer por segundo, pois este valor é necessário na fase de construção. A Figura 4 apresenta este método.

def get_s(self): totalsquares = 0 rsa_ = self.get_rsa() for num in range(0,25):

numbern = rsa_.private_numbers().public_numbers.n numbera = ramdom.SystemRamdom().randint(1, numbern) spersecond = self.compute_s(numbera, numbern) totalsquares = totalsquares + spersecond totalsquares = totalsquares/25

return totalsquares

Ainda neste método criamos, a cada iteração, um número primo n de 2048 bits e um número a, tal que 1 < a < n, e chamamos o método compute_s para calcular quantas operações de quadrado modular é possível realizar em 1 segundo. Em nossos experimentos, foram feitas 25 iterações que, ao final, retornam uma média mais precisa da quantidade de operações por segundo. A quantidade de iterações foi pensada de maneira que não demorasse muito para realizar o cálculo, mas ao mesmo tempo tivessem iterações suficientes para uma média mais precisa do número de operações por segundo.

def compute_s(self, a, n): start_time = time.time() num_squares = 100000

for num in range(0, num_squares): b = pow(a, 2, n)

end_time = time.time()

tot_time = end_time-start_time return num_squares/tot_time

Figura 5 – Método compute_s para Time Lock

Para calcular quantas operações de quadrado modular é possí- vel realizar em 1 segundo, o método compute_s (a Figura 5 apresenta este método) resolve 1.000.000 operações de quadrado modular em um determinando tempo, retornando a média por segundo. A execução destes dois métodos dá origem ao parâmetro S da abordagem.

Agora é preciso inicializar algumas variáveis que serão utilizadas na construção do quebra-cabeça. O método create_variables, mostrado na Figura 6, recebe como parâmetro a mensagem a ser cifrada, o tempo para solução em segundos e o valor obtido na saída do método get_s.

def create_variables(self, message, seconds, totalsquares): rsa_ = self.get_rsa() self.n = rsa_.private_numbers().public_numbers.n self.M = bytes(message) self.T = int(seconds) self.S = int(totalsquares) p = rsa_.private_numbers()._p q = rsa_.private_numbers()._q self.phi = (p - 1) * (q - 1) self.t = self.T * self.S

self.K = self.gen_random_n_digits(16)

As variáveis são inicializadas com os respectivos valores e re- cebem a mesma nomenclatura presente no algoritmo original. É neste método também que os números primos de 2048 bits são criados, dando origem ao valor φ(n). O próximo passo é criar o quebra-cabeça, como exposto na Figura 7.

def create_puzzle(self):

self.a = random.SystemRandom().randint(1, self.n) self.iv = os.urandom(16) K_bytes = self.to_bytes(self.K) enc = Cipher( algorithms.AES(k_bytes), modes.OFB(self.iv), backend = default_backend() ).encryptor()

self.C_M = enc.update(self.M) + enc.finalize() e = pow(2, self.t, self.phi)

b = pow(self.a, e, self.n) self.C_K = self.K + b

return self.C_M, self.iv, self.C_K, self.a, self.t, self.n

Figura 7 – Método create_puzzle para Time Lock

No método create_puzzle, mostrado acima, criamos um objeto cifrador com o algoritmo AES, que irá utilizar a chave K inicializada previamente e um iv aleatório para cifrar a mensagem armazenada (o iv não faz parte da abordagem original, porém é obrigatória a utilização do mesmo ao usar AES no python), dando origem à CM.

O AES (Advanced Encryption Standard) é um padrão de cripto- grafia avançado selecionado pelo NIST (National Institute of Standards and Technology) (NIST, 1901) em 2001. Trata-se de um algoritmo crip-

tográfico (algoritmo de Rijndael (DAEMEN; RIJMEN, 1999)) utilizado

para fins de proteção e segurança de dados compartilhados e mantidos em meio eletrônico (BAJAJ; GOKHALE, 2016).

Agora é necessário cifrar também a chave privada K. Os passos para isto seguem à risca o que foi detalhado no Capítulo 4: Primeiro é gerada a variável a que, elevada à variável e, dá origem à variável b. O método pow do python faz esta exponenciação e já coloca o re- sultado em módulo (o módulo vai depender do número enviado como último parâmetro do método pow). Com isto criamos a variável CK.

O método vai retornar os parâmetros necessários para resolução do quebra-cabeça, conforme visto previamente no detalhamento do algo-

ritmo. Aqui há uma leve diferença do artigo original, pois reforçamos a segurança concatenando um iv à chave inicial, portanto este iv precisa ser enviado agora também.

Tendo o retorno do método create_puzzle é possível começar a solução do quebra-cabeça, através do método solve, que pode ser visto na Figura 8.

def solve(self, iv, message, C_K, a, t, n): b = self.compute_b(a, t, n) key = C_K - b dec = Cipher( algorithms.AES(self.to_bytes(key)), modes.OFB(iv), backend = default_backend() ).decryptor()

return dec.update(message) + dec.finalize() def compute_b(self, a, t, n):

return pow(a, pow(2, t), n)

Figura 8 – Método solve para Time Lock

O método acima computa o valor b chamando um segundo mé- todo compute_b, também exposto na Figura 8. Sabendo o valor de b, é possível subtrair de CK e encontrar a chave privada K. Com esta

chave secreta e o iv podemos decifrar a mensagem e comparar com a original.

6.2.2 Subset Sum

Para construir o quebra-cabeça, neste caso, só é necessário en- viar a sua dificuldade, denominada K. O construtor então inicializa esta variável e cria IDr e Nr, que representam a identidade e o nonce

(número único) do servidor. As credenciais de identificação, tanto do servidor quanto do cliente, devem ser criadas aleatoriamente.

O próximo passo é criar o quebra-cabeça, recebendo como parâ- metro as credenciais do cliente, denominadas IDi e Ni. O método de

criação, cuja assinatura é create_puzzle, cria o valor S, que identifica unicamente a comunicação entre duas partes, de maneira aleatoria, e faz hash de todas as credenciais, obtendo assim a variável hash.

O valor C é então gerado, pegando os K bits menos significati- vos da variável hash obtida anteriormente. Agora, para gerar o peso máximo W desejado, é necessário gerar a série de itens que serão utili- zados na fase de solução. Para isto, o método get_w é chamado, que irá retornar esta coleção. Com isto é possível gerar o quebra-cabeça, multiplicando cada valor de C pelo seu respectivo valor na lista w de pesos. É possível ver o somatório na Figura 9. A função enumerate do python serve para iterar sobre listas com índice numérico, que neste caso é a variável C.

def create_puzzle(self, IDi, Ni):

self.S = self.cryptogen.randint(1000, 999999)

hash = self.get_hash(IDi, Ni, self.Nr, self.IDr, self.S) hash = bin(int(hash, 16))[2:].zfill(8)

self.C = hash[-self.K:]

self.C = [int(i) for i in self.C] w = self.get_w(self.K)

W = 0

for idx, val in enumerate(self.C): W += self.C[idx] * w[idx] return w[0], W, self.K

Figura 9 – Método create_puzzle para Subset Sum

O método para solução do quebra-cabeça segue o mesmo padrão da abordagem anterior, ou seja, os parâmetros de entrada são exata- mente os de saída do método de construção. O primeiro passo é criar todos os pesos da lista, aplicando função hash sobre o peso imediata- mente anterior, k vezes, começando com o item w0 recebido no retorno

do método de construção. O próximo passo requer o uso da primeira bi- blioteca, para que possamos criar a matriz inicial do problema (função create_matrix_from_knapsack_orig no método solve), que é a entrada para o algoritmo LLL. Conforme visto no Capítulo 4, a entrada para esta primeira função da biblioteca deve ser a lista de pesos e o peso máximo suportado, ou seja: w e W , como podemos ver na Figura 10.

Com essa matriz em mãos, composta por bases, fazemos a redu- ção dela com o algoritmo LLL (função lll_reduction no método solve) e, a partir dela, recebemos um conjunto de vetores. Este conjunto de vetores é submetido ao método best_vect_knapsack da biblioteca L3.py, que retorna o menor vetor do conjunto, considerado a resposta

def solve(self, wi, W, k): w = [wi] i = 0 while i < k-1: w.append(int(self.get_hash(w[i]), 16)) i += 1 matrix = create_matrix_from_knapsack_orig(w, W) reduced = lll_reduction(matrix) solution = best_vect_knapsack(reduced) return solution

Figura 10 – Método solve para Subset Sum

para o problema. O caminho de verificação é o mesmo do algoritmo original, comparando a variável obtida no método solve com o valor de C armazenado previamente na memória.

6.2.3 Modular Square Roots

Nesta abordagem o cliente precisa escolher um número primo p ≡ 1 (mod 8) para iniciar a construção do quebra-cabeça. Para isto, no método create_puzzle o cliente pode enviar o tamanho em bits que deseja que o número primo tenha, e o método get_n_bit_prime vai retornar um número primo congruente a 1 mod 8 do tamanho espe- cificado. Com este número em mãos, o cliente segue fazendo hash da mensagem, que é um dos parâmetros de entrada para a construção do problema. Para calcular se o valor d encontrado é um resíduo qua- drático, chama o método calculate_legendre. Este método é o único desta abordagem que foi reaproveitado, pois existem várias fontes para o mesmo. Se não for um resíduo quadrático, o valor será decrementado por 1 até encontrar um que seja. O método create_puzzle pode ser observado na Figura 11.

O próximo passo é chamar o método solve. Neste caso, a única coisa que ele faz é chamar o método cipolla_lehmer, que vai retornar o valor de r, dito como a solução do quebra-cabeça. O método ci- polla_lehmer pode ser observado na Figura 12, junto ao método find_b, que será utilizado para encontrar o valor de b, tal que o resultado de b2 − 4a seja um resíduo não quadrático. Para saber se b é não qua- drático, o método find_b também utiliza o método calculate_legendre. O método find_b então retorna um conjunto de possíveis valores b, no

def create_puzzle(message, nbits): p = get_n_bit_prime(nbits) n = number_of_bits(p) hash_final = get_d(message, n) hash_bits = bin(hash_final) first_bits = hash_bits[2:n+1] d = int(first_bits, 2) is_residue = calculate_legendre(d, p) while is_residue != 1: d = d-1 is_residue = calculate_legendre(d,p) return d, p

Figura 11 – Método create_puzzle para Modular Square Roots

qual um é escolhido aleatoriamente pelo método cipolla_lehmer. Tendo os valores de a, b e p, cria o polinômio x2 − bx + a dentro de um corpo

finito com elementos de 1 a 16 e opera sobre ele. Estes últimos passos foram feitos na sintaxe do sage.

def cipolla_lehmer(a, p): b = find_b(a, p) max_list = len(b)-1 list_position = randint(0,max_list) b = b[list_position] F.<x> = GF(p)[] f = x**2 -b*x + a r = x**( (p+1)/2 ) % f return r def find_b(a, p): set_of_b = [] for b in range(1, p): possible_b = b**2 - 4*a nonresidue = calculate_legendre(possible_b, p) if nonresidue == -1: set_of_b.append(b) return set_of_b

Figura 12 – Método cipolla_lehmer para Modular Square Roots Como entrada do método de verificação dos resultados, temos o resultado r, a mensagem inicial e o primo p. O método check vai pegar o tamanho em bits do parâmetro p para gerar o hash da mensagem recebida e pegar os n − 1 primeiros bits da saída da função hash. Como

explicado no Capítulo 4, não é necessário calcular se é resíduo quadrá- tico. A única verificação é feita escolhendo uma constante (neste caso escolhemos 20, para manter o padrão do algoritmo original) e verifi- cando se esta é maior que o valor d − (r2 (mod p)). Os passos para a

verificação do quebra-cabeça podem ser observados na Figura 13.

def check(message, r, p): n = number_of_bits(p) hash_final = get_d(message, n) hash_bits = bin(hash_final) first_bits = hash_bits[2:n+1] d = int(first_bits, 2) delta = 20

print d - (int(r)^2)%p < delta

Figura 13 – Método check para Modular Square Roots

Os métodos apresentados neste capítulo são os principais para o entendimento do contexto, porém existem métodos secundários que servem de apoio, seja para cálculo de hash, para transformação de obje- tos string em binários ou para fazer iterações sobre listas. No entanto, os conceitos tratados no Capítulo 4 foram todos abordados e serão tes- tados no Capítulo 7.

Vale salientar que, caso haja a necessidade ou curiosidade por parte do leitor de verificar toda a implementação, o código em python está disponível na íntegra como primeiro anexo deste trabalho.

7 RESULTADOS EXPERIMENTAIS 7.1 INTRODUÇÃO

Depois de concluída a implementação dos algoritmos, foi neces- sário rodar os testes produzidos e observar se eles se comportavam de maneira adequada, utilizando a mesma máquina utilizada durante a fase de implementação. A mesma simulou o comportamento do servi- dor e do cliente. Através da execução dos testes também foi possível medir a performance de cada algoritmo, de acordo com os parâmetros de entrada, bem como tirar conclusões sobre a eficiência e aplicabili- dade de cada um, tendo em vista os tempos de execução, tanto para construção como para solução. Para cada uma das abordagens foram executados mais de 20 testes, porém, para tornar este capítulo mais objetivo, vamos expor os resultados encontrados em apenas uma parte dos testes. Para cada abordagem, serão mostrados os valores de, pelo menos, dez testes realizados. O resto deles seguiu o mesmo padrão. 7.2 TIME LOCK

Ao rodarmos os testes para a abordagem de Time Lock, sabíamos que o tempo de solução do quebra-cabeça deveria ser muito próximo ao estipulado, podendo ter pequenas variações de tempo, conforme o artigo original aceita (RIVEST; SHAMIR; WAGNER, 1996).

Teste FornecidoTempos [Segundos]Construção Solução

1 1 0,03 0,92 2 2 0,04 1,84 3 40 0,03 39,98 4 70 0,04 71,12 5 85 0,04 85,05 6 115 0,04 115,73 7 130 0,03 130,05 8 600 0,03 579,07 9 10.800 0,03 10.522,47 10 18.000 0,04 17.948,25

Tabela 1 – Testes para Time Lock

A Tabela 1 mostra, para cada teste executado, o tempo for- necido em fase de construção (que corresponde ao tempo em que o quebra-cabeça deve ser resolvido), o tempo que levou para ser cons-

foi a maior diferença encontrada, como podemos observar na Tabela (1). Já no caso onde o tempo fornecido foi 5 horas (18000 segundos), o erro foi de apenas 52 segundos. Para aumentar a precisão dos resul- tados encontrados, cada um dos testes foi executado mais de uma vez, produzindo também uma média do tempo de solução para cada caso. No maior dos casos testados, para 5 horas, a média retornada para 10 execuções deste teste foi 17.739,08.

Ao analisar o tempo de geração do quebra-cabeça, os resultados também são ótimos, visto que seguem a regra fundamental que diz que o problema deve ser fácil de construir e difícil de resolver. Ao olharmos a coluna "Construção" observamos que nenhum quebra-cabeça demo- rou mais do que 0,04 milésimos de segundo para ser construído, não interessando se o parâmetro de entrada é 10 segundos, ou 18000 segun- dos.

7.3 SUBSET SUM

Nesta abordagem os resultados devem ser interpretados de ma- neira diferente, já que não há como prever o tempo exato em que serão solucionados. A característica forte deste protocolo é que o tempo para solucionar o quebra-cabeça deve aumentar na medida que a dificuldade fornecida nos testes aumenta. Iniciamos nossa rotina de teste com o valor de dificuldade 10 e aumentamos este até 65. Os valores de solu- ção, expostos em segundos, são baixos, mas cresceram rapidamente. O resultados dos testes é mostrado na Tabela 2.

Teste DificuldadeTempos [Segundos]Construção Solução

1 10 0,00 1,25 2 11 0,00 1,56 3 19 0,01 6,79 4 20 0,00 7,96 5 29 0,00 21,10 6 32 0,00 26,87 7 33 0,00 40,51 8 39 0,00 50,12 9 40 0,00 67,34 10 50 0,01 84,33 11 60 0,00 118,74

Tabela 2 – Testes para Subset Sum

cabeça obedece a proporção de aumento entre a dificuldade e o tempo. Já ao analisarmos a coluna "Construção", concluimos que a aborda- gem é ainda mais eficiente para o servidor do que a anterior, visto que a geração do problema é quase instantânea, fazendo com que o servidor não tenha que desprender muitos recursos nesta fase.

A quantidade de tempo é bem pequena, porém segue a mesma linha da versão original criada por Tritilanunt et al. (2007). Ao tes- tarem a própria abordagem os autores concluíram que para qualquer dificuldade menor que 25 o tempo de solução do quebra-cabeça era de 0.0 segundos, ou seja: instantâneo. Para casos onde a dificuldade era maior que 25 e menor que 50 o tempo ainda era muito baixo (exis- tindo casos de solução em 0.1 segundos, como é possível observar na Tabela 3). Os autores só conseguiram um certo nível de dificuldade a partir do valor 50. Ainda segundo os autores da abordagem, não é recomendado usar K > 100, pois o tamanho da matriz de reticulado construída é grande demais, esgotando os recursos de memória.

Itens

Tempo Médio de Execução [segundos]

Conjunto Aleatório 1 Conjunto Aleatório 2 Conjunto Aleatório 3

Densidade Densidade Densidade

0,3 0,4 0,5 0,6 0,7 0,8 0,3 0,4 0,5 0,6 0,7 0,8 0,3 0,4 0,5 0,6 0,7 0,8 60 0,10 0,12 0,23 1,02 2,42 77,11 0,16 0,28 0,19 0,31 3,64 3,70 0,14 0,22 0,21 0,61 0,64 3,21 65 0,14 0,14 0,29 1,59 4,09 190,68 0,18 0,29 0,23 0,57 6,53 6,86 0,17 0,23 0,26 1,70 2,19 18,94 70 0,15 0,15 0,32 2,94 7,33 342,53 0,18 0,29 0,28 1,34 12,97 26,30 0,21 0,25 0,27 2,29 2,29 41,72 75 0,20 0,14 0,78 5,23 13,47 663,24 0,24 0,31 0,38 1,95 27,23 35,65 0,23 0,25 0,34 3,49 4,37 92,37 80 0,27 0,22 0,89 9,63 26,17 1745,97 0,25 0,33 0,52 2,75 58,70 87,12 0,26 0,29 0,45 5,66 8,82 226,76 85 0,37 0,25 1,24 17,38 49,22 4158,73 0,29 0,37 0,72 4,44 120,44 208,86 0,28 0,32 0,62 9,40 18,15 1315,29 90 0,50 0,29 1,63 31,44 96,39 9435,02 0,39 0,40 1,17 7,58 250,52 509,60 0,30 0,37 0,89 16,42 37,75 1344,35 95 0,59 0,34 2,34 55,68 173,30 21351,72 0,43 0,43 1,75 12,78 504,88 1158,45 0,36 0,43 1,28 28,14 79,36 3160,86 100 0,70 0,40 3,43 98,39 317,27 51124,86 0,46 0,47 2,87 21,45 1008,23 2737,79 0,41 0,50 2,03 46,63 168,72 7451,26

Tabela 3 – Resultado Experimental do Subset Sum (TRITILANUNT et al., 2007)

7.4 MODULAR SQUARE ROOTS

Já esta abordagem deve ser analisada de acordo com o número primo escolhido para geração do problema. Quanto maior o número, maior deve ser o tempo desprendido para a solução.

Assim como nas abordagens anteriores, podemos comprovar, atra- vés dos testes, que a aplicação funciona de acordo com o desejado. O tempo para solução do quebra-cabeça cresce de acordo com a dificul- dade de encontrar a raíz quadrada de um elemento módulo um número primo. Isto acontece pois, quanto maior o número primo, maior a quan- tidade de elementos a serem testados dentro do corpo finito.

Teste Número primoTempos [Segundos]Construção Solução 1 17 0,00 0,00 2 97 0,00 0,07 3 233 0,00 0,06 4 281 0,00 0,08 5 313 0,00 0,8 6 769 0,00 1,2 7 7481 0,01 6,9 8 12073 0,01 17,6 9 98953 0,02 596,4 10 2459489 0,04 4345,8

Tabela 4 – Testes para Modular Square Roots

Conforme os autores da abordagem afirmam em seu trabalho, testar se um elemento é resíduo quadrático ou não quadrático é bastante complexo para grandes números primos. Esta tarefa está diretamente ligada com o tempo de espera para a solução do quebra-cabeça. A si- tuação fica ainda mais demorada quando o primeiro número escolhido acaba sendo não-quadrático. Isto significa que é necessário decremen- tar este número por 1 e testar novamente todas as possibilidades de elementos dentro do conjunto.

Através dos resultados foi possível perceber que números primos pequenos não fornecem muita dificuldade para o cliente. Nestes casos, mesmo que a solução do quebra-cabeça estivesse correta, o servidor provavelmente negaria o acesso a seus serviços, solicitando ao cliente que escolhesse um número primo maior para o quebra-cabeça e tentasse novamente.

Os testes com números primos com 30 bits demoraram algumas horas para serem solucionados e, ao tentar rodar um teste utilizando um número primo de 512 bits, não houve recurso de memória suficiente. Dito isto, é possível afirmar a dificuldade do problema proposto, bem como comprovar a eficácia deste para cenários de negação de serviço, onde o objetivo do mesmo é fazer com que o cliente realize um grande esforço computacional, medindo este esforço pelo tamanho do número primo escolhido.

8 CONSIDERAÇÕES FINAIS

Este trabalho teve como objetivo o desenvolvimento de um pro- grama que implementasse três algoritmos de quebra-cabeça criptográ- fico estudados. Antes de iniciar a programação, foi feito um longo estudo sobre o cenário atual de segurança em computação, avaliando a importância de compreender e utilizar mecanismos como os que foram aqui explicados. Após a finalização do programa, foi possível atestar a eficácia de cada um deles através de resultados experimentais obtidos com rotinas de testes bem definidas para cada abordagem.

Além de detalhar todos os aspectos matemáticos, práticos e con- ceituais dos três algoritmos escolhidos, foi exposto também diversos ou- tros protocolos de quebra-cabeça criptográfico existentes, levando em conta as características de cada um deles e a aplicabilidade dentro do vasto ambiente de segurança e sigilo da informação nos dias de hoje.

Nos capítulos 5 e 6 podemos observar como foi organizado todo o projeto do código e como as funções principais funcionam, evidenciando os métodos de criação, solução e verificação de resultado. Além disso, observações sobre os testes unitários realizados foram feitas, compro- vando que todos estão funcionando de acordo com o esperado. Ficou claro que as características apresentadas para cada implementação são bastante diferentes, e a escolha por utilizar qualquer um desses métodos para reforçar os requisitos de segurança deve ser baseada nas necessi- dades específicas de cada caso.

Foi possível concluir, analisando a pesquisa feita em cima das te- orias de quebra-cabeças criptográficos existentes, que esses mecanismos são realmente importantes, visto que podem ser aplicados em diferen- tes conceitos e, portanto, podem auxiliar no reforço da segurança em diversas situações diferentes, desde envio de mensagens no futuro até evitar ataques de negação de serviço. Quanto mais mecanismos volta- dos para a proteção contra ataques maliciosos existirem, melhor.

Além disto, analisando os testes executados em cada uma das abordagens implementadas, foi possível perceber a eficiência dos algo- ritmos, que funcionam corretamente de acordo com o escopo no qual foram criados. Em uma breve comparação podemos concluir que cada um possui vantagens dependendo do contexto em que se deseja aplicar.

Por exemplo, em um contexto onde a prioridade é ter pouco custo por parte do servidor, as abordagens Subset Sum e Modular Square Roots são mais adequadas, enquanto em cenários onde a precisão é prioritá- ria, a escolha pelo Time Lock deve ser considerada a melhor.

Por fim, tendo em mente o exposto acima quanto à importân- cia de quebra-cabeças criptográficos (bem como quaisquer outros meios de proteger-se), é coerente afirmar que a contribuição deste trabalho é, também, importante dentro deste âmbito. Quanto mais materiais, fontes, exemplos e códigos existirem implementando esse tipo de me- canismo, mais os usuários terão ferramentas disponíveis para aprender,

No documento Quebra-Cabeças Criptográficos (páginas 71-86)

Documentos relacionados