Guia de Python para criptografia
Introdução
Python está entre as linguagens de programação mais utilizadas da atualidade, dentre os principais motivos estão: legibilidade, simplicidade e flexibilidade. Especialmente para criptografia, Python inclui, por padrão, muitas funções e ferramentas que facilitam o trabalho do programador/aventureiro.
O objetivo deste guia será introduzir os principais conceitos dessa linguagem tendo como público-alvo pessoas que já possuam experiência em outras linguagens, como C/C++. Para informações mais detalhadas e ferramentas não mencionadas neste guia, favor consultar a documentação oficial do Python em: https://docs.python.org/3/
Executando códigos
Comandos do Python podem ser executados de três formas: fornecendo um arquivo (de extensão .py) que já contém o código a ser executado, inserindo comandos interativamente pelo terminal do Python, ou combinando as duas técnicas ao importar um arquivo já existente através do terminal e utilizar as variáveis e funções declaradas nesse arquivo, interativamente.
No primeiro caso, utilizando um terminal localizado no mesmo diretório onde se encontra o arquivo, executamos:
No segundo caso, simplesmente executamos python
e inserimos os comandos desejados no terminal, os quais são interpretados em tempo real. Neste caso, funções e variáveis chamadas mas não atribuídas imprimem seu conteúdo diretamente no terminal.
No terceiro caso, executamos:
e inserimos comandos da mesma forma do modo anterior, porém com a vantagem de já possuirmos disponível todo o código declarado no referido arquivo.
Observação: No Ubuntu e distribuições Linux derivadas dele, a versão padrão do Python é a 2, portanto os comandos acima executariam o Python 2. Para utilizar a versão 3 (mais recomendada) é necessário utilizar o comando “python3”. Muitos dos exemplos listados neste guia não funcionarão caso executados na versão 2!
Escopos
Diferentemente de outras linguagens, Python não utiliza colchetes para determinar os escopos do código, ele obtém essa informação através da indentação. Ou seja, linhas de código que estão no mesmo “nível” horizontal estarão no mesmo escopo de execução. Exemplo:
Esse código imprime apenas “escopo externo”, pois o primeiro print, que foi chamado no escopo do if, não é executado.
Tipos de dados
Python difere de linguagens como C ao possuir tipagem dinâmica. Em outras palavras, o tipo de uma variável pode ser alterada, sem qualquer restrição, em tempo de execução. Essa regra também vale para estruturas complexas que armazenam outros tipos, que serão descritas abaixo.
Além disso, Python possui tipagem forte, ou seja, ele não permite fazer operações entre tipos de dados diferentes. Por exemplo, em C a operação ‘b’ - 1 retorna o inteiro pertencente ao código ASCII de ‘a’ (97), porém em Python essa operação gera um erro de execução. Para isso é necessário utilizar as funções chr() e ord() que retornam, respectivamente, o caractere de um código ASCII e o código ASCII de um caractere.
Mutaveis
Tipos de dados mutáveis permitem que o valor de seus elementos e sua própria estrutura sejam alterados em qualquer momento da execução do código. Os exemplos mais usados para criptografia são:
Listas
Listas são caracterizadas como uma sequência ordenada de elementos de qualquer tipo. São análogas ao array (vetor) do C/C++, porém, assim como tudo em Python, listas permitem misturar tipos diferentes dentro da mesma estrutura.
Neste exemplo, criamos uma lista L com os tipos String, Integer e Boolean. Qualquer posição da lista pode ser alterada dinamicamente, similarmente ao C. Para informações detalhadas sobre tipos iteráveis, ler a seção 8 deste guia.
Dicionários
Dicionários são análogos ao map do C++, ou seja, é um tipo que associa pares chave-valor. As chaves devem ser únicas, porém os valores podem se repetir.
Bytearrays
Juntamente com o tipo bytes (explicado na seção 4.2.c), o bytearray é especialmente importante para criptografia por facilitar o processo de encriptação e decriptação de mensagens e exibição de ciphertexts. Um bytearray pode ser visto como uma “lista de bytes”, onde cada elemento assume um valor entre 0 e 255 (um byte).
Sua representação na função print() é feita através de caracteres de “byte puro” em hexadecimal quando estes não são imprimíveis, ou de caracteres ascii quando são; ou seja, “\x02” é o terceiro caractere não imprimível (incluindo o zero), “\x03” é o quarto, etc. A seção 6 deste guia tratará com mais detalhes esse tipo de representação. Porém, ao ser referenciado individualmente, um elemento de bytearray é exibido como inteiro decimal entre 0 e 255 (um byte).
A habilidade de transformar bytearrays em uma string de hexadecimais é interessante quando se deseja imprimir um ciphertext na tela. A maioria dos bytes de um ciphertext não se encontram no intervalo de caracteres imprimíveis da tabela ASCII, portanto a representação hexadecimal ajuda nessa tarefa. Além disso, a operação de xor entre duas strings é facilitada utilizando esse tipo, e será ensinada com mais detalhes na seção 8.2 deste guia.
O construtor (função que cria o objeto) do tipo bytearray necessita receber um objeto iterável ou um inteiro como argumento. No primeiro caso, cria-se um bytearray com todos os elementos do iterável. No segundo caso, cria-se um bytearray com n elementos iguais a zero, onde n é o inteiro fornecido como argumento. Portanto caso se deseje criar um bytearray com um único elemento, é necessário incluí-lo dentro de uma lista ou tupla:
Imutáveis
Tipos imutáveis são caracterizados pelo fato de não permitirem alterações em seus elementos após terem sido declarados, com exceção do método extend() ou do operador += (ambos realizam a inclusão de novos elementos), a qual é permitida irrestritamente. A vantagem da utilização de tipos imutáveis é que eles são os únicos tipos que permitem o cálculo de hash, de forma que, por exemplo, somente tipos imutáveis podem ser a chave de um dicionário; além disso, elas apresentam uma maior confiabilidade e um maior desempenho de execução em comparação aos mutáveis, por motivos que fogem ao escopo deste guia.
Strings
Strings em Python são semelhantes às strings de qualquer linguagem de programação de alto nível, sendo a principal diferença para outras linguagens o fato de elas serem imutáveis. Em outras palavras, podemos concatenar novas strings ao final de outra já existente, mas não podemos alterar qualquer parte dela diretamente; para isso é necessário convertê-la em uma lista e realizar as operações desejadas sobre a lista, e em seguida convertê-la de volta a uma string utilizando o método ''.join(lista).
Simplificadamente, o método join de uma string s que recebe como argumento uma lista l retorna uma nova string com os elementos de l separados por s; neste caso, como s é uma string vazia, obteremos uma nova string apenas com os elementos de l. Esse método será mais explorado no exemplo a seguir.
Strings literais em Python podem ser representadas por aspas duplas ou simples, portanto a string ‘exemplo’ é exatamente igual à string “exemplo”.
Tuplas
Tuplas são os equivalentes imutáveis das listas, ou seja, podem ser vistas como listas cujos elementos não podem ser alterados.
Bytes
Bytes é o equivalente imutável do bytearray. Ao contrário do bytearray, o tipo bytes possui uma representação literal.
As mesmas regras de construção do bytearray também se aplica ao tipo bytes:
Conversões entre bases
Dada uma string representando um número em uma base qualquer, é possível convertê-la em decimal utilizando a função int(), onde o primeiro argumento é a string e o segundo argumento indica a base onde essa string está codificada:
Também é possível representar números de diversas bases de forma literal no Python. Para binário utilizamos o prefixo 0b, para octal utilizamos o prefixo 0o, para hexadecimal utilizamos o prefixo 0x, e assim por diante. Porém, esses literais são sempre automaticamente convertidos em decimal ao armazenarmos seus valores numa variável ou imprimirmos eles na tela:
Também podemos utilizar as funções bin(), oct() e hex() para converter decimais em binário, octal e hexadecimal, respectivamente. Porém todas essas funções retornam uma string, visto que o Python não armazena valores literais em outras bases:
Como visto anteriormente, dado um objeto do tipo bytearray ou bytes, é possível convertê-lo em uma sequência de hexadecimais, e vice-versa:
Codificação e decodificação
Por padrão, o Python utiliza a codificação utf-8, isso significa que podemos armazenar strings com caracteres especiais (incluindo acentuação, por exemplo).
Os caracteres de uma string que estão contidos na tabela ascii são codificados como ascii (pois eles coincidem com o utf-8) e utilizam apenas 1 byte por caractere, porém os caracteres especiais necessitam de mais bytes para serem representados. Por conta disso, se armazenarmos uma string com caracteres especiais em um objeto do tipo bytes e utilizarmos a função print() nesse objeto, esses caracteres não serão imprimidos na tela, ao invés disso os seus bytes individuais serão imprimidos com o prefixo ‘\x’, isso indica que eles são caracteres fora da tabela ascii e possuem o valor em hexadecimal representado após o ‘\x’.
Como dito anteriormente, a fim de obtermos o código ascii ou unicode de um caractere no Python devemos utilizar a função ord(). Analogamente, para obtermos o caractere correspondente a um código unicode devemos utilizar a função chr().
Operadores
Os operadores mais usados em criptografia são o XOR e o módulo. O primeiro é muito utilizado para encriptar e decriptar mensagens, e o segundo é utilizado para garantir que um cálculo específico não ultrapasse determinado valor, geralmente o intervalo de um byte (de 0 a 255).
XOR
Assim como em C, o operador XOR (^) opera sobre dois inteiros e retorna o resultado de aplicar ou-exclusivo (eXclusive-OR) sobre eles, bit a bit (bitwise).
Módulo
Assim como em C, o operador módulo (%) opera sobre dois inteiros e retorna o resto da divisão do primeiro pelo segundo, ou seja, X % Y retorna o resto da divisão de X por Y.
Loops e iteradores
For
O for do Python difere consideravelmente do for da linguagem C pois, no Python, este deve sempre iterar sobre outros objetos “iteráveis” (o nome genérico para esse tipo de loop é for each).
Todos os tipos apresentados na seção 4 e os generators, explicados na seção 8.3, são iteráveis, ou seja, seus elementos podem ser acessados invidualmente, em ordem, portanto todos podem ser utilizados no for.
A sintaxe do for é, de forma genérica, for {variável} in {iterador}, sem os colchetes, e como resultado faz com que, a cada iteração, “variável” receba o valor de cada elemento presente no objeto “iterador”.
Para utilizar o for do Python da mesma forma que utilizamos em C, fazemos uso da função range() que recebe como argumento 3 valores (start, stop e step) e retorna um iterador contendo todos os valores entre start e (stop - 1), separados por step. Então ao executarmos for i in range(0, 10, 1), o valor de i receberá, a cada iteração, os valores de 0 a 9, separados de 1 em 1. Contudo, os argumentos start e step são opcionais e, caso não sejam fornecidos, são definidos como 0 e 1, respectivamente.
A iteração também pode ser feita em dois ou mais objetos simultaneamente utilizando a função zip(). Essa função retorna n tuplas, onde cada tupla contém os elementos de mesma posição dos objetos que foram fornecidos à função, e “n” é o tamanho do menor desses objetos. Caso um dos objetos seja maior do que o outro, os elementos restantes do maior objeto serão ignorados.
Outro método de iteração simultânea é utilizando a função enumerate() que recebe apenas 1 objeto de tamanho n e retorna n tuplas, cada uma contendo a posição e o valor de cada elemento do objeto.
Xor entre bytes
Agora que sabemos o funcionamento do for e do bytes, podemos finalmente implementar um código que realiza a operação xor entre dois objetos bytes quaisquer. Operação essa que é muito útil para diversos algoritmos de criptografia. Lembrando que o operador xor é representado pelo circunflexo (^) e sempre opera sobre dois decimais inteiros.
Generators
Generators são um tipo especial de iterador. Além de possuírem uma sintaxe bastante compacta, sua principal característica que os diferem de outros iteradores é que eles não armazenam elementos em sua estrutura e portanto quase não consomem espaço na memória RAM, ao invés disso, ao se solicitar o próximo elemento de um generator ele faz o cálculo e devolve o resultado requisitado sob demanda.
Existem 2 tipos principais de generators:
O primeiro utiliza a sintaxe {expressão} for {variável} in {iterador}, sem os colchetes. Nesse caso, “variável” itera sobre cada elemento do “iterador”, e este generator retorna o resultado de “expressão” (possui acesso ao conteúdo de “variável”), sob demanda, a cada requisição de uma nova iteração. Essa sintaxe pode ser atribuída a uma variável, a qual será do tipo generator e poderá ser utilizada para solicitar novos valores, ou pode ser utilizada para construir outros tipos, como listas, tuplas e bytes.
O segundo tipo utiliza funções e será explicado na seção 9.1.
Agora somos capazes de reescrever o mesmo código da seção 8.2 (xor entre bytes) utilizando apenas 1 linha:
Slices
Slice é um recurso do Python que permite acessar “pedaços” de objetos iteráveis (exceto generators). Seu formato é [começo:fim:passo], onde começo indica a posição inicial que se deseja obter, (fim - 1) é a posição final que se deseja obter, e passo é a diferença entre elementos consecutivos. Caso o passo não seja fornecido, seu valor é definido como 1. Caso o começo não seja fornecido, seu valor é definido como a posição inicial do objeto. Caso o fim não seja fornecido, seu valor é definido como a posição final do objeto.
Funções
As funções no Python são semelhantes às funções de outras linguagens, com diferença de que o tipo de retorno, assim como tudo Python, não precisa ser especificado.
Para definir uma função utilizamos a keyword def, seguidos pelos parâmetros entre parênteses, e utilizamos a mesma regra de escopo da seção 3 para definir todo o código que pertence à função:
Generator
Como foi citado na seção anterior, é possível criar funções do tipo generator, as quais realizam um cálculo e devolvem a expressão após a keyword yield a cada requisição de uma nova iteração do gerador:
Bibliotecas de criptografia
PyCryptodome
PyCryptodome é uma biblioteca que surgiu para substituir a PyCrypto, que foi descontinuada. Atualmente ela possui diversas cifras simétricas e assimétricas (Salsa20, ChaCha, RSA, entre outras), funções de Hash e de Assinatura Digital, chaves públicas, gerador de números pseudoaleatórios, entre outros.
Para instalar essa biblioteca no Python, execute no terminal pip install --user pycryptodome
, a flag --user
realiza a instalação apenas para o usuário logado é importante para garantir que ela não seja instalada em todo o sistema, o que possivelmente pode causar problemas futuros. Obs: Caso você esteja utilizando o comando python3
para executar códigos, utilize pip3
para instalar módulos.
A API com todas as ferramentas dessa biblioteca está disponível em https://www.pycryptodome.org/en/latest/src/api.html.
Exemplos de uso dessa biblioteca podem ser encontrados em https://www.pycryptodome.org/en/latest/src/examples.html.
Last updated