La criptografía se ocupa de cifrar o codificar una información (en un texto sin formato) en una forma que parece un galimatías y tiene poco sentido en el lenguaje común.

Este mensaje codificado (también llamado  texto cifrado ) puede luego ser descodificado de nuevo en un texto sin formato por el destinatario deseado usando una técnica de descodificación (a menudo junto con una clave privada) comunicada al usuario final.

Caesar Cipher es una de las técnicas de cifrado más antiguas en las que nos centraremos en este tutorial y la implementaremos en Python .
Aunque Caesar Cipher es una  técnica de cifrado muy débil  y rara vez se usa hoy en día, estamos haciendo este tutorial para presentarles a nuestros lectores, especialmente a los recién llegados, el cifrado. Considere esto como el 'Hola mundo' de la criptografía.

¿Qué es Caesar Cipher?

Caesar Cipher es un tipo de cifrado de sustitución , en el que cada letra del texto sin formato se reemplaza por otra letra en algunas posiciones fijas de la letra actual en el alfabeto.

Por ejemplo, si desplazamos cada letra 3 posiciones a la derecha, cada una de las letras de nuestro texto sin formato será reemplazada por una letra en 3 posiciones a la derecha de la letra en el texto sin formato.
Veamos esto en acción: cifremos el texto "HOLA MUNDO" con un desplazamiento a la derecha de 3.

Entonces, la letra H será reemplazada por K, E será reemplazada por H, y así sucesivamente. El mensaje encriptado final para  HELLO WORLD  será  KHOOR ZRUOG. Ese galimatías no tiene sentido, ¿verdad?

Tenga en cuenta que las letras en el borde, es decir, X, Y, Z se envuelven y se reemplazan por A, B, C respectivamente, en el caso del desplazamiento a la derecha. De manera similar, las letras al principio: A, B, C, etc. se envolverán en caso de cambios a la izquierda.

La  regla de cifrado Caesar Cipher  se puede expresar matemáticamente como:

1
c = (x + n) % 26

Donde c es el carácter codificado, x es el carácter real y n es el número de posiciones en las que queremos desplazar el carácter x. Tomamos mod con 26 porque hay 26 letras en el alfabeto inglés.

Cifrado César en Python

Antes de sumergirnos en la definición de las funciones para el proceso de cifrado y descifrado de Caesar Cipher en Python, primero veremos dos funciones importantes que usaremos ampliamente durante el proceso: chr ()  y  ord () .
Es importante darse cuenta de que el alfabeto tal como lo conocemos se almacena de manera diferente en la memoria de una computadora. La computadora no entiende nada del alfabeto de nuestro idioma inglés u otros caracteres por sí misma.

Cada uno de estos caracteres se representa en la memoria de la computadora mediante un número llamado código ASCII (o su extensión, el Unicode) del carácter, que es un número de 8 bits y codifica casi todos los caracteres, dígitos y signos de puntuación del idioma inglés.
Por ejemplo, la 'A' mayúscula está representada por el número 65, la 'B' por 66, y así sucesivamente. De manera similar, la representación de los caracteres en minúsculas comienza con el número 97.

A medida que surgió la necesidad de incorporar más símbolos y caracteres de otros idiomas, los 8 bits no fueron suficientes, por lo que se adoptó un nuevo estándar,  Unicode  , que representa todos los caracteres utilizados en el mundo utilizando 16 bits.
ASCII es un subconjunto de Unicode, por lo que la codificación ASCII de caracteres sigue siendo la misma en Unicode. Eso significa que 'A' todavía estará representada usando el número 65 en Unicode.
Tenga en cuenta que los caracteres especiales como el espacio "", las pestañas "\ t", las líneas nuevas "\ n", etc. también están representados en la memoria por su Unicode.

Veremos 2 funciones integradas en Python que se utilizan para encontrar la representación Unicode de un carácter y viceversa.

La función ord ()

El método ord () se utiliza para convertir un carácter a su representación numérica en Unicode. Acepta un solo carácter y devuelve el número que representa su Unicode. Veamos un ejemplo.

1
2
3
4
5
6
7
c_unicode = ord("c")
 
A_unicode = ord("A")
 
print("Unicode of 'c' =", c_unicode)
 
print("Unicode of 'A' =", A_unicode)

Salida:

La función chr ()

Así como podríamos convertir un carácter en su Unicode numérico usando el método ord (), hacemos lo inverso, es decir, encontramos el carácter representado por un número usando el método chr ().
El método chr () acepta un número que representa el Unicode de un carácter y devuelve el carácter real correspondiente al código numérico.
Veamos primero algunos ejemplos:

01
02
03
04
05
06
07
08
09
10
11
character_65 = chr(65)
 
character_100 = chr(100)
 
print("Unicode 65 represents", character_65)
 
print("Unicode 100 represents", character_100)
 
character_360 = chr(360)
 
print("Unicode 360 represents", character_360)

Salida:

Observe cómo la letra alemana  Ü  (diéresis U) también se representa en Unicode, con el número 360.

También podemos aplicar una operación encadenada (ord seguida de chr) para recuperar el carácter original.

1
2
3
c = chr(ord("Ũ"))
 
print(c)

Salida: Ũ

Cifrado de letras mayúsculas

Ahora que entendemos los 2 métodos fundamentales que usaremos, implementemos la técnica de cifrado para letras mayúsculas en Python. Encriptaremos solo los caracteres en mayúscula en el texto y dejaremos los restantes sin cambios.
Primero veamos el proceso paso a paso de cifrar las letras mayúsculas:

  1. Defina el valor de cambio, es decir, el número de posiciones que queremos cambiar de cada carácter.
  2. Repita cada carácter del texto sin formato:
    1. Si el carácter está en mayúsculas:
      1. Calcule la posición / índice del carácter en el rango 0-25.
      2. Realice el  cambio positivo  utilizando la operación de módulo.
      3. Encuentra al personaje en la nueva posición.
      4. Reemplaza la letra mayúscula actual por este nuevo carácter.
    2. De lo contrario, si el carácter no está en mayúsculas, manténgalo sin cambios.

Veamos ahora el código:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
shift = 3 # defining the shift count
 
text = "HELLO WORLD"
 
encryption = ""
 
for c in text:
 
    # check if character is an uppercase letter
    if c.isupper():
 
        # find the position in 0-25
        c_unicode = ord(c)
 
        c_index = ord(c) - ord("A")
 
        # perform the shift
        new_index = (c_index + shift) % 26
 
        # convert to new character
        new_unicode = new_index + ord("A")
 
        new_character = chr(new_unicode)
 
        # append to encrypted string
        encryption = encryption + new_character
 
    else:
 
        # since character is not uppercase, leave it as it is
        encryption += c
         
print("Plain text:",text)
 
print("Encrypted text:",encryption)

Salida:

Como podemos ver, el texto encriptado de “HELLO WORLD” es “KHOOR ZRUOG”, y coincide con el que llegamos manualmente en la sección de Introducción. Observe también que el carácter de espacio no se ha cifrado y sigue siendo un espacio en la versión cifrada.

Descifrado de letras mayúsculas

Ahora que hemos descubierto el cifrado de letras mayúsculas de texto sin formato usando Ceaser Cipher, veamos cómo desciframos el texto cifrado en texto sin formato.

Anteriormente analizamos la formulación matemática del proceso de cifrado. Veamos ahora lo mismo para el proceso de descifrado.

1
x = (c - n) % 26

El significado de las notaciones sigue siendo el mismo que en la fórmula anterior.Si
algún valor se vuelve negativo después de la resta, el operador de módulo puede encargarse de lo mismo y los valores se envolverán

Veamos la implementación paso a paso del proceso de descifrado, que será más o menos el reverso del cifrado:

  • Definir el número de turnos
  • Repita cada carácter del texto cifrado:
    • Si el carácter es una letra mayúscula:
      1. Calcule la posición / índice del carácter en el rango 0-25.
      2. Realice el  cambio negativo  usando la operación de módulo.
      3. Encuentra al personaje en la nueva posición.
      4. Reemplace la letra cifrada actual por este nuevo carácter (que también será una letra mayúscula).
      5. De lo contrario, si el carácter no es mayúscula, manténgalo sin cambios.

Escribamos el código para el procedimiento anterior:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
shift = 3 # defining the shift count
 
encrypted_text = "KHOOR ZRUOG"
 
plain_text = ""
 
for c in encrypted_text:
 
    # check if character is an uppercase letter
    if c.isupper():
 
        # find the position in 0-25
        c_unicode = ord(c)
 
        c_index = ord(c) - ord("A")
 
        # perform the negative shift
        new_index = (c_index - shift) % 26
 
        # convert to new character
        new_unicode = new_index + ord("A")
 
        new_character = chr(new_unicode)
 
        # append to plain string
        plain_text = plain_text + new_character
 
    else:
 
        # since character is not uppercase, leave it as it is
        plain_text += c
 
print("Encrypted text:",encrypted_text)
 
print("Decrypted text:",plain_text)

Salida:

Observe cómo hemos recuperado con éxito el texto original "HOLA MUNDO" de su forma cifrada.

Cifrar números y puntuación

Ahora que hemos visto cómo podemos codificar y decodificar letras mayúsculas del alfabeto inglés usando Caesar Cipher, surge una pregunta importante: ¿qué pasa con los otros caracteres?
¿Y los números? ¿Qué pasa con los caracteres especiales y la puntuación?

Bueno, se suponía que el algoritmo Caesar Cipher original no debía tratar con nada más que las 26 letras del alfabeto, ya sea en mayúsculas o minúsculas.
Por lo tanto, un cifrado típico de Caesar no cifraría la puntuación ni los números y convertiría todas las letras a minúsculas o mayúsculas y codificaría solo esos caracteres.

Pero siempre podemos extender una buena solución existente y ajustarla para que se adapte a nuestras necesidades, eso es cierto para cualquier tipo de desafío en la ingeniería de software.
Así que intentaremos codificar caracteres en mayúsculas y minúsculas como lo hicimos en la sección anterior, ignoraremos las puntuaciones por ahora y luego también codificaremos los números en el texto.

Para los números, podemos realizar el cifrado de una de las dos formas siguientes:

  1. Cambie el valor del dígito en la misma cantidad que cambia las letras del alfabeto, es decir, para un cambio de 3 dígitos, 5 se convierte en 8, 2 se convierte en 5, 9 se convierte en 2, y así sucesivamente.
  2. Haga que los números formen parte del alfabeto, es decir, z o Z ​​irán seguidos de 0,1,2… hasta 9 y esta vez nuestro divisor para la operación de módulo será 36 en lugar de 26.

Implementaremos nuestra solución utilizando la primera estrategia. También esta vez, implementaremos nuestra solución como una función que acepta el valor de cambio (que sirve como clave en Caesar Cipher) como parámetro.
Implementaremos 2 funciones:  cipher_encrypt ()  y  cipher_decrypt ()
¡ Ensuciemos nuestras manos!

La solución

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
sesenta y cinco
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# The Encryption Function
def cipher_encrypt(plain_text, key):
 
    encrypted = ""
 
    for c in plain_text:
 
        if c.isupper(): #check if it's an uppercase character
 
            c_index = ord(c) - ord('A')
 
            # shift the current character by key positions
            c_shifted = (c_index + key) % 26 + ord('A')
 
            c_new = chr(c_shifted)
 
            encrypted += c_new
 
        elif c.islower(): #check if its a lowecase character
 
            # subtract the unicode of 'a' to get index in [0-25) range
            c_index = ord(c) - ord('a')
 
            c_shifted = (c_index + key) % 26 + ord('a')
 
            c_new = chr(c_shifted)
 
            encrypted += c_new
 
        elif c.isdigit():
 
            # if it's a number,shift its actual value
            c_new = (int(c) + key) % 10
 
            encrypted += str(c_new)
 
        else:
 
            # if its neither alphabetical nor a number, just leave it like that
            encrypted += c
 
    return encrypted
 
# The Decryption Function
def cipher_decrypt(ciphertext, key):
 
    decrypted = ""
 
    for c in ciphertext:
 
        if c.isupper():
 
            c_index = ord(c) - ord('A')
 
            # shift the current character to left by key positions to get its original position
            c_og_pos = (c_index - key) % 26 + ord('A')
 
            c_og = chr(c_og_pos)
 
            decrypted += c_og
 
        elif c.islower():
 
            c_index = ord(c) - ord('a')
 
            c_og_pos = (c_index - key) % 26 + ord('a')
 
            c_og = chr(c_og_pos)
 
            decrypted += c_og
 
        elif c.isdigit():
 
            # if it's a number,shift its actual value
            c_og = (int(c) - key) % 10
 
            decrypted += str(c_og)
 
        else:
 
            # if its neither alphabetical nor a number, just leave it like that
            decrypted += c
 
    return decrypted

Ahora que hemos definido nuestras 2 funciones, primero usemos la función de encriptación para encriptar un mensaje secreto que un amigo está compartiendo a través de un mensaje de texto con su amigo.

1
2
3
4
5
6
7
plain_text = "Mate, the adventure ride in Canberra was so much fun, We were so drunk we ended up calling 911!"
 
ciphertext = cipher_encrypt(plain_text, 4)
 
print("Plain text message:\n", plain_text)
 
print("Encrypted ciphertext:\n", ciphertext)

Salida:

Observe cómo se ha cifrado todo, excepto la puntuación y los espacios.

Ahora veamos un texto cifrado que el coronel Nick Fury estaba enviando en su localizador: ' Sr xli gsyrx sj 7, 6, 5 - Ezirkivw Ewwiqfpi! ¡
Resulta que es el texto cifrado de César y, afortunadamente, tenemos en nuestras manos la clave de este texto cifrado!
Veamos si podemos desenterrar el mensaje oculto.

1
2
3
4
5
6
7
ciphertext = "Sr xli gsyrx sj 7, 6, 5 - Ezirkivw Ewwiqfpi!"
 
decrypted_msg = cipher_decrypt(ciphertext, 4)
 
print("The cipher text:\n", ciphertext)
 
print("The decrypted message is:\n",decrypted_msg)

Salida:

¡Así se hace, Vengadores!

Usando una tabla de búsqueda

En esta etapa, hemos entendido el proceso de cifrado y descifrado de Caesar Cipher y lo hemos implementado en Python.

Ahora veremos cómo se puede hacer más eficiente y flexible.
Específicamente, nos centraremos en cómo podemos evitar los cálculos repetidos de las posiciones cambiadas para cada letra en el texto durante el proceso de cifrado y descifrado, mediante la creación de una  tabla de búsqueda con  anticipación.

También veremos cómo podemos acomodar cualquier conjunto de símbolos definidos por el usuario y no solo las letras del alfabeto en nuestro proceso de cifrado.
También fusionaremos el proceso de cifrado y descifrado en una función y aceptaremos como parámetro cuál de los dos procesos desea ejecutar el usuario.

¿Qué es una tabla de búsqueda?

Una tabla de búsqueda es simplemente un mapeo de los caracteres originales y los caracteres a los que deberían traducirse de forma cifrada.
Hasta ahora hemos estado iterando sobre cada una de las letras de la cadena y calculando sus posiciones cambiadas.
Esto es ineficaz porque nuestro conjunto de caracteres es limitado y la mayoría de ellos ocurren más de una vez en la cadena.
Por lo tanto, calcular su equivalencia encriptada cada vez que ocurren no es eficiente y se vuelve costoso si estamos encriptando un texto muy largo con cientos de miles de caracteres.

Podemos evitar esto calculando las posiciones cambiadas de cada uno de los caracteres en nuestro conjunto de caracteres solo una vez, antes de comenzar el proceso de cifrado.
Entonces, si hay 26 letras mayúsculas y 26 minúsculas, solo necesitaríamos 52 cálculos una vez y algo de espacio en la memoria para almacenar este mapeo.
Luego, durante el proceso de cifrado y descifrado, todo lo que tendríamos que hacer es realizar una 'búsqueda' en esta tabla, una operación que es más rápida que realizar una operación de módulo cada vez.

Crear una tabla de búsqueda

El  módulo de cadenas de Python  proporciona una manera fácil no solo de crear una tabla de búsqueda, sino también de traducir cualquier cadena nueva basada en esta tabla.

Tomemos un ejemplo en el que queremos crear una tabla de las primeras cinco letras minúsculas y sus índices en el alfabeto.
Luego usaríamos esta tabla para traducir una cadena donde cada una de las apariciones de 'a', 'b', 'c', 'd' y 'e' se reemplazan por '0', '1', '2' , '3' y '4' respectivamente; y los personajes restantes están intactos.

Usaremos la función  maketrans ()  del   módulo str para crear la tabla.
Este método acepta como primer parámetro, una cadena de caracteres para los que se necesita traducción y otro parámetro de cadena de la misma longitud que contiene los caracteres asignados para cada carácter en la primera cadena.

Creemos una tabla para un ejemplo simple.

1
table = str.maketrans("abcde", "01234")

La tabla es un diccionario de Python, que tiene los valores Unicode de los caracteres como claves y sus correspondientes asignaciones como valores.
Ahora que tenemos nuestra tabla lista, podemos traducir cadenas de cualquier longitud usando esta tabla.
Afortunadamente, la traducción también es manejada por otra función en el módulo str, llamada  translate.

Usemos este método para convertir nuestro texto usando nuestra tabla.

1
2
3
4
5
6
7
text = "Albert Einstein, born in Germany, was a prominent theoretical physicist."
 
translated = text.translate(table)
 
print("Original text:/n", text)
 
print("Translated text:/n", translated)

Salida:

Como puede ver, cada instancia de las primeras 5 letras minúsculas ha sido reemplazada por sus índices relativos

Ahora usaremos la misma técnica para crear una tabla de búsqueda para Caesar Cipher, basada en la clave proporcionada.

Implementando el cifrado

Vamos a crear una función  caesar_cipher ()  que acepte que una cadena sea cifrada / descifrada, el 'conjunto de caracteres' que muestra qué caracteres de la cadena deben cifrarse (esto será por defecto en letras minúsculas),
la clave y un valor booleano que muestre si se descifra ha realizado o no (cifrado).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
import string
 
def cipher_cipher_using_lookup(text,  key, characters = string.ascii_lowercase, decrypt=False):
 
    if key < 0:
 
        print("key cannot be negative")
 
        return None
 
    n = len(characters)
 
    if decrypt==True:
 
        key = n - key
 
    table = str.maketrans(characters, characters[key:]+characters[:key])
     
    translated_text = text.translate(table)
     
    return translated_text

¡Esa es una función poderosa!

Toda la operación de cambio se ha reducido a una operación de corte.
Además, estamos usando el   atributo string.ascii_lowercase : es una cadena de caracteres de 'a' a 'z'.
Otra característica importante que hemos logrado aquí es que la misma función logra tanto el cifrado como el descifrado, esto se puede hacer cambiando el valor del parámetro 'clave'.
La operación de corte junto con esta nueva clave asegura que el conjunto de caracteres se haya desplazado a la izquierda, algo que hacemos en el descifrado de un texto cifrado César con desplazamiento a la derecha.

Validemos si esto funciona usando un ejemplo anterior.
Encriptaremos solo letras mayúsculas del texto y proporcionaremos lo mismo al parámetro 'caracteres'.
Encriptaremos el texto: “¡HOLA MUNDO! ¡Bienvenido al mundo de la criptografía! "

1
2
3
4
5
text = "HELLO WORLD! Welcome to the world of Cryptography!"
 
encrypted = cipher_cipher_using_lookup(text, 3, string.ascii_uppercase, decrypt=False)
 
print(encrypted)

Salida:

Compruebe cómo la parte "KHOOR ZRUOG" coincide con el cifrado de "HELLO WORLD" con la clave 3 en nuestro primer ejemplo.
Además, tenga en cuenta que estamos especificando el conjunto de caracteres para que sean letras mayúsculas usando  string.ascii_uppercase

Podemos comprobar si el descifrado funciona correctamente utilizando el mismo texto cifrado que obtuvimos en nuestro resultado anterior.
Si podemos recuperar nuestro texto original, eso significa que nuestra función funciona perfectamente.