Cesar falando de modo cifrado

Cifra de César: o que é e como implementar em Python

No artigo de hoje, vamos implementar um programinha em Python que cifra e decifra mensagens utilizando a famosa Cifra de César. Essa é uma das mais antigas e mais rasteiras técnicas de criptografia da humanidade, que consiste em substituir cada letra da mensagem a ser cifrada por uma letra que esteja X posições depois no alfabeto.

Se você quiser utilizar o sistema substituindo as letras pelas que se encontram duas posições depois, por exemplo, você deve trocar A por C, B por D, etc.

E então dizemos que a chave do seu código é o número 2, porque para cifrá-lo andamos duas letras para frente, e, por consequência, para recuperar a mensagem original, basta que andemos duas letras para trás.

É possível implementar esse programa de várias formas. Aqui, vou demonstrar uma forma bem concisa (menos de 15 linhas!) . Para isso, vamos utilizar alguns macetes e coisas interessantes da linguagem Python e de programação em geral, que talvez você não conheça, e que vão melhorar suas habilidades como programador. Ao longo do tutorial, você vai aprender, dentre outras coisas:

  • A utilizar as funções ord() e chr() para manipular símbolos Unicode.
  • O que é expressão condicional (ou operador ternário) em Python.
  • Macetes com o operador módulo (ou resto de divisão).

Mas atenção: não utilize esse código se você quiser criptografar suas mensagens para se comunicar de uma forma efetivamente segura! A criptografia moderna utiliza métodos muito mais complexos que esse.

Então vamos lá!

Em primeiro lugar, precisamos estabelecer como exatamente nosso programa vai funcionar. O que queremos é criar uma função que cifre uma determinada mensagem e uma função outra que decifre a mensagem codificada, com base numa certa chave que será fornecida pelo usuário. A função que cifra deve percorrer cada letra da mensagem e substituí-la pela letra X posições depois, e retornar a string resultante desse processo. A função que decifra deve fazer mais ou menos a mesma coisa, mas “andando para trás”.

Como precisamos substituir uma letra por outra baseado num certo número que define a quantidade de letras que devemos “pular”, podemos intuir que a forma mais prática de fazer isso seria transformando as letras em números para então realizar operações aritméticas sobre elas. Podemos, por exemplo, atribuir a cada letra, um valor de 0 a 25 (considerando que o alfabeto tem 26 letras), em que A = 0, B = 2, C = 3…

Nesse caso, se temos a palavra AMADOR, e queremos cifrá-la pulando duas letras (leia-se, utilizando o número 2 como chave de codificação), devemos fazer as operações: A+2, M+2, A+2, D+2, O+2, R+2, para chegar na mensagem criptografada COCFQT.

Poderíamos implementar esse procedimento de várias formas, mas acontece que já existe um sistema muito conhecido de conversão de números para caracteres, o onipresente padrão Unicode.

O padrão Unicode

Não vou entrar em muitos detalhes pra não complicar, mas o que interessa é que o Unicode é um padrão amplamente utilizado que descreve mais de 130 mil símbolos, dentre os quais, emojis, símbolos matemáticos e símbolos das mais diversas línguas. Nesse sistema, cada símbolo é representado por número, chamado de code point.

Então não precisamos criar nosso próprio sistema de conversão, porque o Unicode já fez esse trabalho pra a gente. E o que é melhor, o Python já vem de fábrica com duas funções que servem justamente para fazer essas conversões: são as funções ord() e chr().

As funções ord() e chr()

A função ord() recebe como argumento um caractere (em Python isso quer dizer “uma string de tamanho 1”) e retorna seu valor decimal correspondente em Unicode. Já a função chr() recebe um número inteiro como argumento, e retorna o caractere Unicode correspondente.

Com isso já podemos brincar um pouquinho. Abra o interpretador do Python ou crie um arquivo .py e coloque o código abaixo:

for codigo in range(33,127):
    print("{:<3}: {}".format(codigo, chr(codigo)))
 

Agora rode o programa e veja o que acontece. O que esse código faz é enumerar todos os sinais gráficos do padrão Unicode, do 33 ao 126 (os sinais de 0 a 32 são sinais de controle, como quebra de linha e backspace, e coisas estranhas podem acontecer quando tentamos imprimi-los).

O conjunto de caracteres Unicode do 0 ao 126 são, diga-se de passagem, o subconjunto dos caracteres ASCII, mas essa já é outra história.

Se você tentar mudar o número 127 no código acima para, digamos, 256, vai ver que vão aparecer letras acentuadas e outros símbolos mais esquisitos. Se você continuar explorando, verá símbolos ainda mais exóticos, como uma bomba de gasolina (code point 9981), ou um floco de neve (10052).

Analisando a saída do programa, podemos perceber que o que nos interessa são os códigos entre 65 e 90 e entre 97 e 122, que são as letras do alfabeto maiúsculas e minúsculas, respectivamente. Poderíamos fazer um programa que aceitasse tanto letras maiúsculas quanto minúsculas, mas para simplificar, vamos trabalhar só com um grupo de letras. Vou escolher as maiúsculas.

Perceba também que os caracteres acentuados não se encontram nessa lista. Como eles são símbolos diferentes, eles se encontram em outra parte da lista de símbolos, (as letras acentuadas maiúsculas estão localizadas entre o 193 e o 218). Como as letras acentuadas estão fora da sequência de letras do alfabeto, a melhor solução é ignorar a acentuação em nosso algoritmo. Afinal, a que letra corresponderia um Á+3? Um D com acento? Não daria certo!

Desenvolvendo o programa

Agora, vamos começar a desenvolver nosso programa propriamente dito. Crie um arquivo .py e vamos lá. Comecemos criando uma lista com o nosso alfabeto. Poderíamos definir essa lista manualmente, mas como somos programadores e gostamos de automatizar coisas chatas, vamos fazer uso de uma facilidade do módulo string, do Python. Essa biblioteca tem uma série de constantes interessantes relacionadas a strings. Uma delas é a constante ascii_uppercase, uma string contendo todas as letras maiúsculas do alfabeto. Justamente o que queremos! Para utilizá-la, basta escrever:

from string import ascii_uppercase

Se você escrever um print(ascii_uppercase) e rodar o programa, verá que ele imprime todas as letras maiúsculas do alfabeto, sem espaço. Agora, basta transformar essa string em uma lista com a função list():

alfabeto = list(ascii_uppercase)

Pronto! Temos a lista com as letras que serão usadas para codificar e decodificar as mensagens. Agora, podemos nos referir à letra A como alfabeto[0], à letra B como alfabeto[1] e por aí vai. Se o usuário inserir uma mensagem e quiser codificá-la com a chave 3, basta trocar cada letra da mensagem por alfabeto[número correspondente à letra +3]. Mas como sabemos o número a que corresponde cada letra? Usando a função ord(), como veremos daqui a pouco!

Para o nosso primeiro protótipo, adicione ao código que você já escreveu uma variável chamada mensagem, contendo a mensagem a ser codificada (lembre de utilizar nesse exemplo somente letras maiúsculas sem acentuação). Eu utilizarei “AMADOR”. Crie também uma variável chamada chave, contendo um número inteiro, que será a chave de codificação. Usarei a chave número 4 no exemplo. Por último, crie uma variável chamada cifra, para armazenar a mensagem cifrada, ela será inicializada com uma string vazia, ou seja, um par de aspas sem nada dentro (inclusive espaços!). Desse jeito:

mensagem = "AMADOR"
chave = 4
cifra = ""

Agora, precisamos criar um laço para percorrer cada letra da mensagem e adicionar à string cifra o caractere adequado. Em Python iterar sobre strings é muito simples. Basta criarmos um laço for da seguinte forma:

for letra in mensagem:
…

Isso faz com que a cada iteração do for, um caractere da variável mensagem seja armazenado em letra, do início até o fim da string.

Muito bem, agora precisamos transformar cada letra em um número utilizando a função ord(). Mas como os códigos Unicode das letras maiúsculas começam a partir do número 65, precisamos subtrair esse número do resultado da função ord() para chegarmos ao índice correto de nossa lista alfabeto.

Por exemplo: em nossa lista, a letra B está localizada em alfabeto[1]. Quando utilizamos ord(“B”), ele retornará o valor 66, ao invés de 1. Basta fazermos a operação 66 – 65 para chegarmos ao resultado correto. Em outras palavras, alfabeto[ord(“B”)-65] == “B”. Convença-se disso antes de continuar!

Para testar nossa lógica, vamos adicionar as seguintes linhas dentro do laço for:

    indice = ord(letra)-65
    print (alfabeto[indice])
 

Se você tiver feito tudo certinho e rodar o programa, deverá ver a mensagem “AMADOR”, escrito uma letra em cada linha.

Ótimo, mas não é isso que queremos! Queremos cifrar a mensagem de acordo com a chave. Para isso, basta que peguemos o elemento indice+chave da lista alfabeto, ao invés de pegarmos apenas o indice.

Também não queremos apenas imprimir o resultado da codificação enquanto a mensagem está sendo codificada. Queremos armazenar a codificação na variável cifra. Para isso, podemos apensar a nova letra à variável cifra a cada iteração do for com a operação cifra += alfabeto[indice+chave] (Lembre-se que x+= y é equivalente a x = x + y em Python).

O nosso programa completo, até o momento, fica assim:

from string import ascii_uppercase

alfabeto = list(ascii_uppercase)

mensagem = "AMADOR"
chave = 4
cifra = ""

for letra in mensagem:
    indice = ord(letra)-65
    cifra+= alfabeto[indice+chave]

print(cifra)

Rodando o programa, você verá a misteriosa mensagem EQEHSV.

Resolvendo problemas de índice (index out of range)

Muito bem! Nosso programa está funcionando! Agora vamos aumentar nossa mensagem. Vamos escrever “AMADOR PROGRAMA” ao invés de “AMADOR” e rodar novamente.

Traceback (most recent call last):
  File "ascii.py", line 11, in <module>
    cifra+= alfabeto[indice+chave]
IndexError: list index out of range

Opa, deu pau! O que será que aconteceu? Acontece que agora na nossa mensagem temos um espaço. O caractere “espaço” tem o código Unicode 32. Quando fazemos a operação indice = ord(letra)-65, temos 32-65, que dá -33. Então, na linha seguinte, tentamos acessar alfabeto[-33+4], que não existe. Por isso o interpretador nos retorna um erro de índice.

Para resolver o problema, precisamos inserir um condicional, dizendo que caso a letra pertença ao alfabeto, o programa deve fazer a operação de codificação, caso contrário, deverá inseri-la na string cifra do jeito que está. Em outras palavras, devemos ignorar os caracteres que não são letras maiúsculas não acentuadas, colocando elas na mensagem do jeito que aparecem.

Expressões condicionais em Python

Podemos fazer isso com um if normal, mas essa é uma boa oportunidade para utilizar o operador ternário, também conhecido como expressão condicional. O operador ternário em Python tem a estrutura:

x = y if <condição> else w 

O que quer dizer: se uma certa <condição> for satisfeita, x = y, caso contrário, x = w. Utilizando esse recurso em nosso código, precisaremos modificar apenas uma linha:

cifra+= alfabeto[indice+chave] if letra in alfabeto else letra

Simples e elegante! Traduzindo para o português, essa expressão diz que cifra deve ser igual a cifra concatenada com alfabeto[indice+chave] se o valor da variável letra estiver contido na lista alfabeto, ou igual a cifra concatenada com o valor da variávelletra, caso este não esteja contido na lista alfabeto.

Se você fizer essa modificação e rodar o programa, verá que vai rodar direitinho e produzir a mensagem misteriosa: EQEHSV TVSKVEQE.

Estamos fazendo progressos! Vamos fazer mais um teste. Troque a mensagem por: AMADOR PROGRAMA EM LINUX (sem ofensa!). Agora rode o programa…

Traceback (most recent call last):
  File "ascii.py", line 11, in <module>
    cifra+= alfabeto[indice+chave] if letra in alfabeto else letra
IndexError: list index out of range

Droga! Outro erro de índice. O que houve? O problema agora ocorre quando o programa tenta computar a cifra de X. O código do X é 88. E 88-65 = 23. Quando tentamos acessar alfabeto[23+4] dá erro porque nossa lista vai do índice 0 (que corresponde ao A) ao 25 (que corresponde ao Z).

Como o Z é a última letra do alfabeto, com uma chave de valor 4, teremos problemas com as letras W X Y Z, porque não podemos avançar além do Z para fazer essa codificação. Com uma chave maior, teremos problemas com ainda mais letras. Precisamos encontrar um modo de fazer com que, depois do Z, a contagem recomece a partir do A outra vez. Assim, 4 letras a partir do X, teríamos a letra B.

Poderíamos utilizar alguns if e elif para testar se indice+chave > 25 e contornar o problema em caso positivo. É uma solução legítima, mas existe outra alternativa, mais concisa que essa. Podemos utilizar o operador módulo (não confundir com valor absoluto!).

O operador módulo

O operador de módulo é também conhecido como resto de divisão. E em Python, seu símbolo é o %. E ele faz exatamente isso: retorna o resto de uma divisão entre dois números inteiros (embora ele também tenha seus meandros). Mas porque isso seria útil pra nós?

Se você parar pra pensar, sempre que fazemos algum tipo de contagem em que depois de um valor limite, o contador retorna a 0, estamos fazendo uma operação análoga ao de restos de divisão de um número cada vez maior por um número fixo. A afirmação parece estranha?

Tomemos como exemplo um relógio digital. Ele vai marcando as horas de 00 até 23. Depois que ele ultrapassa a marca de 23 horas, ele retorna ao 0. O comportamento dos dígitos que representam as horas, no relógio digital, é equivalente ao do resto de uma divisão de números cada vez maiores por 24:

0 divido por 24 (0//24) é igual a 0, e sobra 0.

1//24 = 0 e sobra 1.

2//24 = 0 e sobra 2…

23//24 = 0 e sobra 23.

Finalmente, 24//24 = 1 e sobra 0.

25//24 = 1 e sobra 1.

26//24 = 1 e sobra 2…

Ou seja, quando o número do contador chega a 24, voltamos ao resto zero e a contagem recomeça.

Se a analogia ainda não parece clara, digite o seguinte programinha no interpretador do Python:

for i in range(100):
        print(i%5)

Com esse simples código, acabamos de criar um mecanismo de contador que conta de 0 a 4, recomeçando a contagem a cada vez que passa do número 4. Não precisamos usar nenhum if pra isso!

O comportamento que queremos em nosso programa é basicamente esse. Se o valor da letra somado ao valor da chave for superior ao número de elementos em alfabeto, queremos dar “uma volta no relógio”, e recomeçar a contagem a partir do 0. Em outras palavras, ao invés de alfabeto[indice+chave], queremos alfabeto[(indice+chave)%len(alfabeto)].

len() é uma função que vai retornar o tamanho da lista alfabeto, em nosso caso, 26. Se estivermos processando a letra Z (índice = 25) e a chave for 1, o índice acessado será (25+1) % 26, que é igual a 0. O que corresponde à letra A.

Faça essa pequena modificação no programa, e tente rodar. Agora deve dar certo!

O programa completo agora deve estar assim:

from string import ascii_uppercase

alfabeto = list(ascii_uppercase)

mensagem = "AMADOR PROGRAMA EM LINUX"
chave = 4
cifra = ""

for letra in mensagem:
    indice = ord(letra)-65
    cifra+= alfabeto[(indice+chave) %len(alfabeto)] if letra in alfabeto else letra

print(cifra)

Perceba que fizemos todo o algoritmo da codificação propriamente dita em apenas 3 linhas de código (as linhas correspondentes ao escopo do laço for! São poucas linhas, mas que fazem muita coisa.

Muito bem, já temos nosso script para codificar mensagens. Mas pra ficar mais prático, vamos encapsular esse algoritmo em uma função.

from string import ascii_uppercase

alfabeto = list(ascii_uppercase)

def codifica(mensagem, chave):
    cifra = ""
    for letra in mensagem:
        indice = ord(letra)-65
        cifra+= alfabeto[(indice+chave) %len(alfabeto)] if letra in alfabeto else letra
    return cifra

Pronto! Agora só precisamos fazer uma função para decodificar nossas mensagens cifradas.

O Decifrador de César

Poderíamos adaptar nossa função codifica para que ela funcionasse subtraindo o valor da chave do valor do índice, fazendo com que ela “andasse para trás” nos itens da lista alfabeto. Essa é uma boa saída. Mas perceba uma coisa. Por exemplo, se eu tenho uma chave de decodificação de valor 2, isso significa que a letra C viraria A, a D viraria B, a E viraria C, etc. Mas isso não é a mesma coisa que eu ter uma chave de codificação de tamanho 24?

Vejamos, a letra D corresponde ao code point 68, o que significa que ‘D’ é o item 3 da nossa lista alfabeto (68-65 = 3). Então, o índice da lista alfabeto correspondente à letra ‘D’ com a chave 24 seria (3+24) % 26 = 1. E 1 é justamente o índice correspondente à letra B. O que prova que voltar duas casas pra trás é equivalente a andar 24 pra a frente!

Mas que bruxaria é essa? Bom, como já vimos, utilizar o operador módulo é como dar voltas no relógio. Então, se eu uso a chave de codificação de número 26, que é o tamanho da nossa lista, nós vamos parar exatamente no mesmo lugar, porque 26 % 26 = 0. Do mesmo modo que o ponteiro do relógio vai se encontrar no mesmo lugar em que está 24 horas depois.

Se usando a chave 26 eu volto pro mesmo lugar, consequentemente, se eu uso como chave o valor 26 – n, é como se eu parasse no mesmo lugar e voltasse n casas. Deu pra entender?

Então, podemos implementar nossa função decodifica() como uma wrapper function que recebe a chave de decodificação e chama a função codifica com o valor len(alfabeto) – chave de decodificação:

def decodifica(mensagem, chave):
    return codifica(mensagem, len(alfabeto)-chave)

Simples, não? Já temos um cifrador/decifrador de César funcional em Python. O código completo fica assim (Nota: pra facilitar a sua vida, você pode baixar o código desse e dos outros tutoriais do blog direto do repositório do blog no Github):

from string import ascii_uppercase

alfabeto = list(ascii_uppercase)

def codifica(mensagem, chave):
    cifra = ""
    for letra in mensagem:
        indice = ord(letra)-65
        cifra+= alfabeto[(indice+chave) %len(alfabeto)] if letra in alfabeto else letra
    return cifra

def decodifica(mensagem, chave):
    return codifica(mensagem, len(alfabeto)-chave)

13 linhas! Para testar nosso programa, vamos utilizá-lo como um módulo. Salve o código como cesar.py. Em seguida, abra uma janela de terminal e altere o diretório corrente para o diretório em que você salvou o código. Agora, execute o interpretador Python no terminal e escreva o seguinte código:

import cesar

for i in range(50):
    mensagem = "AMADOR PROGRAMA!"
    cifra = cesar.codifica(mensagem, i)
    print(cifra, ":", cesar.decodifica(cifra, i))

O resultado será a mensagem “AMADOR PROGRAMA!” cifrada com todas as chaves de 0 a 49, ao lado da mensagem decodificada.

Claro que poderíamos fazer alguns ajustes. Em nosso cifrador, se o usuário chamar a função com uma mensagem escrita apenas com letras minúsculas, o programa não fará nada, porque foi configurado pra ignorar tudo que não sejam as letras maiúsculas do alfabeto. O ideal seria que ele convertesse as letras para maiúsculas antes de fazer a conversão.

Também poderíamos criar uma espécie de pré-processador, que verificasse se a mensagem contém alguma letra acentuada e substituísse por sua versão sem acento. Por exemplo:

VOCÊ TOCA ÓRGÃO? → VOCE TOCA ORGAO?

Nosso programa também ignora números, deixando-os do mesmo jeito na mensagem cifrada. E se estivermos tentando enviar uma mensagem cifrada com as coordenadas das catapultas do César? Seria interessante que tivessemos um sistema que cifrasse números utilizando a mesma técnica que utilizamos para as letras.

Vou deixar essa parte como dever de casa!

Mais uma coisa: os mais curiosos podem ter percebido que nosso esquema de decodificação funciona até mesmo quando a chave de decodificação é maior que o tamanho do alfabeto, e consequentemente, passamos para a função codifica() uma chave negativa. Por exemplo, se a chave de decodificação for 48, então a função decodifica() vai chamar a função codifica() utilizando -22 como parâmetro (26 – 48) e por algum milagre isso funciona, pois para o Python -22 % 26 = 4.

Isso se deve ao modo como o operador % é implementado no Python, e eu precisaria escrever um post inteiro para explicar os detalhes de como isso funciona (está na minha lista de pendências!). Mas se você ficou interessado, vale dar uma pesquisada na internet pra descobrir o motivo!

Related Posts

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *