Cifrado Cesar en python (Caesar Cipher)

Cifrado Cesar en python
Cifrado Cesar en python

Hoy vamos a ver un poco de Criptografía con python. Lo que haremos será crear un script en python que encripte o desencripte (dependiendo de la opción que elijamos) un texto en español que nosotros ingresemos.

Tanto el material como el código Python fueron traducidos de la página: http://http://inventwithpython.com

Un poco de historia

El gobernante Julio Cesar utilizó un simple cifrado para las comunicaciones secretas. El cifrado consistía en sustituir cada letra del alfabeto por otra tres lugares más adelante. Luego, todo cifrado que utiliza este concepto de desplazamiento para la creación de un mensaje cifrado se llamó "Cifrado Cesar" (Caesar Cipher). De todos los sistemas de cifrado de tipo sustitución, el cifrado Cesar es el más simple de resolver, ya que existen 25 combinaciones posibles.

Desplazamiento en el Cifrado Cesar
Desplazamiento en el Cifrado Cesar

Debido a que este programa manipula texto para convertirlo en un mensaje secreto, aprenderemos nuevas funciones y métodos para trabajar con las cadenas. También veremos como el programa realiza matemáticas con las cadenas de texto del mismo modo que lo hace con los números.

Criptografía

La escritura de códigos secretos de denomina Criptografía. Durante muchos años la criptografía se ha implementado para crear mensajes secretos con el fin de que el remitente y el destinatario sean los únicos que puedan leerlos, incluso si el mensaje es interceptado "no se podrá leer".
En criptografía se llama texto plano al texto que queremos que sea secreto, un ejemplo de texto plano sería:

Hola, gracias por visitar Mi Diario Python

Nosotros convertiremos el texto plano en un mensaje cifrado. El texto cifrado parecerá un texto al azar y no podremos entender el mensaje original a simple vista. Aquí esta el ejemplo del texto plano anterior cifrado:

Mtqf, lwfhnfx utw anxnyfw Rn Infwnt Udymts

Pero si nosotros sabemos que tipo de codificación se utiliza para cifrar el mensaje, podremos convertir el texto cifrado en el texto original.
Muchos cifrados también utilizan llaves. Estas llaves son secretas y son la forma que tenemos para descifrar el mensaje. Pensemos en el mensaje cifrado como una puerta trancada, para abrirla necesitamos la llave correspondiente.

Código para cifrar mensajes

#!/usr/bin/python
# -*- coding: utf-8 -*-

# www.pythondiario.com
# Cifrado Cesar

LONG_LLAVE = 26


def getOpcion():

 while True:
  print '¿Tu quieres encriptar o desencriptar un mensaje?'
  opcion = raw_input().lower()
  if opcion in 'encriptar e desencriptar d'.split():
   return opcion
  else:
   print 'Escribir "encriptar", "e" o "desencriptar", "d".'


def getMensaje():
 print 'Escribe el mensaje:'
 return raw_input()

def getLlave():
 llave = 0
 while True:
  print('Ingresar la llave (1-%s)' % (LONG_LLAVE))
  llave = int(input())
  if (llave >= 1 and llave <= LONG_LLAVE):
   return llave

def getTraducirMensaje(opcion, mensaje, llave):
 if opcion[0] == 'd':
  llave = -llave
 traducir = ''
 
 for simbolo in mensaje:
  if simbolo.isalpha():
   num = ord(simbolo)
   num += llave

   if simbolo.isupper():
    if num > ord('Z'):
     num -= 26
    elif num < ord('A'):
     num += 26
   elif simbolo.islower():
    if num > ord('z'):
     num -= 26
    elif num < ord('a'):
     num += 26

   traducir += chr(num)
  else:
   traducir += simbolo
 return traducir



opcion = getOpcion()
mensaje = getMensaje()
llave = getLlave()

print('El texto traducido es:')
print(getTraducirMensaje(opcion, mensaje, llave))

El cifrado Cesar

La llave para el cifrado Cesar será un número del 1 al 26. A menos que sepamos el número de la llave, no podremos descifrar el mensaje.
Este sistema fue uno de los primeros inventados para cifrar mensajes. Si corremos la letra A un espacio obtenemos la letra B, si corremos dos lugares la letra A obtenemos la letra C.
En la siguiente imagen se muestra un desplazamiento de 3 lugares:

Desplazamiento de 3 lugares
Desplazamiento de 3 lugares

El número de espacios que cambia la letra es la llave. En el ejemplo anterior la llave sería el número 3.

Cifrando Mensaje
Cifrando Mensaje

En la imagen anterior ejecuté el programa y elegí la opción encriptar (el mensaje a cifrar fue "abcde") y la llave que elegí fue 3 para explicar mejor lo del ejemplo anterior. El resultado fue el texto cifrado: defgh

  • La letra a fue sustituida por la letra d
  • La letra b fue sustituida por la letra e
  • La letra c fue sustituida por la letra f
  • La letra d fue sustituida por la letra g
  • La letra e fue sustituida por la letra h

Para desencriptar (descifrar) el mensaje, hacemos lo mismo pero elegimos la opción desencriptar (con la llave correspondiente, en este caso 3):

Descifrando Mensaje
Descifrando Mensaje

En nuestro cifrado utilizaremos código ASCII

Las letras mayúsculas de la "A" a la "Z" tienen los números ASCII del 65 al 90. Las letras minúsculas de la "a" a la "z" tienen los números ASCII del 97 al 122. Los dígitos numéricos del "0" al "9" tienen los números ASCII del 48 al 57.

O sea que si queremos desplazar la letra A 3 espacios, tendríamos que hacer lo siguiente:

  1. Convertir la letra A en el número 65
  2. Sumarle 3 para correrla 3 espacios (68)
  3. Convertir el número 68 a letra (D)

Las funciones chr() y ord() son las responsables de convertir los números a letras y viceversa.

Tabla ASCII
Tabla ASCII

Las funciones chr() y ord()

La función chr() toma un número entero y devuelve una cadena de un solo caracter. La función ord() toma una cadena de un solo caracter y devuelve un entero. Veamos algunos ejemplos en el shell interactivo:

>>> chr(67)
"C"
>>> ord("C")
67
>>> chr(67+8)
"K"
>>> chr(52)
"4"
>>> chr(ord("F"))
"F"
>>> ord(chr(68))
68

Como funciona nuestro programa

Primera linea de código:

LONG_LLAVE = 26

Esta es una simple variable que almacena el número 26. La llave que utiliza el sistema de cifrado debe ser un número entre el 1 y 26.

Decidir entre encriptar o desencriptar un mensaje

def getOpcion():

 while True:
  print '¿Tu quieres encriptar o desencriptar un mensaje?'
  opcion = raw_input().lower()
  if opcion in 'encriptar e desencriptar d'.split():
   return opcion
  else:
   print 'Escribir "encriptar", "e" o "desencriptar", "d".'

La función getOpcion() interactua con el usuario para ver si quiere encriptar o desencriptar un mensaje. La opción elegida por el usuario se guarda en la variable opcion. El condicional if chequea si existe la opción deseada ingresada por el usuario. El usuario tendrá que escribir la cadena encriptar o e (para cifrar un mensaje) o tendrá que escribir desencriptar o d (para descifrar un mensaje). El método split() convierte la cadena "encriptar e desencriptar d" en una lista.


Ingreso de mensaje por el usuario

def getMensaje():
 print 'Escribe el mensaje:'
 return raw_input()

La función getMensaje() guarda el mensaje que ingresa el usuario ya sea para cifrar o descifrar.

Obtener la llave del usuario

def getLlave():
 llave = 0
 while True:
  print('Ingresar la llave (1-%s)' % (LONG_LLAVE))
  llave = int(input())
  if (llave >= 1 and llave <= LONG_LLAVE):
   return llave

La función getLlave() le pide al usuario el tipo de llave para cifrar o descifrar un mensaje. El bucle while se asegura de que el usuario ingrese un número mayor o igual a 1 y menor o igual a 26 (el número que contiene la variable LONG_LLAVE).


Encriptar o desencriptar un mensaje con la clave dada

def getTraducirMensaje(opcion, mensaje, llave):
 if opcion[0] == 'd':
  llave = -llave
 traducir = ''

La función getTraducirMensaje() contiene 3 parámetros:

  • La opción elegida por el usuario (cifrar o descifrar)
  • El mensaje a cifrar o descifrar
  • La llave que se utiliza para cifrar o descifrar el mensaje

La función comprueba si la primera letra de la cadena que esta en la variable opcion es la "d". Si esto es correcto, el programa queda en modo descifrado. La diferencia entre el modo cifrado y descifrado es que el modo descifrado convierte la llave en un valor negativo de si mismo.
La variable traducir es la cadena que se obtiene como resultado: texto cifrado (si va a encriptar) o texto plano (si va a desencriptar). Esta variable comienza vacía. 

El método isalpha() para las cadenas

El método isalpha() devuelve True si la cadena contiene solo letras (minúsculas y mayúsculas) de la A a la Z. Si la cadena contiene otros caracteres el método isalpha() devolverá False. Probemos algunos ejemplos en el shell interactivo:

>>> "Python".isalpha()
True

>>> "Python Diario".isalpha()
False

>>> "PythonDiario".isalpha()
True

>>> "34".isalpha()
False

>>> "".isalpha()
False

Como podemos ver, "Python Diario".isalpha() devuelve False porque existe un espacio entre las cadenas (por lo tanto no es una letra). "PythonDiario".isalpha() devuelve True porque la cadena no contiene espacios.
El método isalpha() es utilizado en las siguientes líneas de código:

for simbolo in mensaje:
  if simbolo.isalpha():
   num = ord(simbolo)
   num += llave

Este for itera sobre cada letra del mensaje asegurando que sea una letra con el método isalpha(). Los números, signos de puntuación, etc, quedarán en su forma original. La variable num guardará la letra convertida a número por la función ord(). Y por último se suma la llave para desplazar la letra.

Los métodos isupper() e islower()

Los métodos isupper() e islower() funcionan igual que los métodos isdigit() e isalpha().
El método isupper() devolverá True si la cadena tiene al menos una letra en mayúsculas y no contiene minúsculas. El método islower() devolverá True si la cadena tiene al menos una letra en minúscula y no contiene mayúsculas. De otra forma, estos métodos devuelven False:

>>> "PYTHON".isupper()
True

>>> "python".isupper()
False

>>> "python".islower()
True

>>> "PYTHON DIARIO".isupper()
True

>>> "42".isupper()
False

>>> "42".islower()
False

Cifrar o Descifrar cada letra

if simbolo.isupper():
 if num > ord('Z'):
  num -= 26
 elif num < ord('A'):
  num += 26

La primera línea comprueba si el caracter es una letra Mayúscula. Si este es el caso, hay dos cosas a tener en cuenta. Imaginemos que el caracter es "Z" y la llave 4, el caracter de la variable num en este caso sería '^' (al sumar 90 ("Z") + 4 (llave) nos da 94, chr(94) = '^'). Por lo tanto '^' no es una letra.
Si es mayor (>) a "Z", entoces resta 26 a la variable num (porque hay 26 letras). Después de hacer esto, el valor de num queda en 68 (94 - 26), que sería la letra "D".

elif simbolo.islower():
 if num > ord('z'):
  num -= 26
 elif num < ord('a'):
         num += 26

Si la letra es minúscula, esta parte hace los mismo que el código anterior. La única diferencia es que se utiliza ord("z") y ord("a") en ves de ord("Z") y ord("A").

        traducir += chr(num)
else:
 traducir += simbolo

En estas líneas se concatena la letra cifrada/descifrada a la variable traducir. Si el caracter no era una letra Mayúscula o Minúscula (traducir += simbolo), se concatena el caracter a la variable traducir.

 return traducir

La útima línea de la función getTraducirMensaje() devuelve el mensaje traducido.

Arrancar el programa

opcion = getOpcion()
mensaje = getMensaje()
llave = getLlave()

print('El texto traducido es:')
print(getTraducirMensaje(opcion, mensaje, llave))

Estas líneas de codigo llaman a las funciones declaradas anteriormente para obtener la opción, el mensaje y la llave. Estos tres valores son pasados a la función getTraducirMensaje(), que como ya vimos, imprime en pantalla el mensaje traducido.

Fuerza Bruta

Terminamos con el análisis del código. Sin embargo, aunque este cifrado/descifrado pueda engañar a muchas personas que no entiendan de criptografía, no va a ser un secreto para personas que sepan algo de criptoanálisis.

Supongamos que nuestro mensaje cifrado cae en manos de una persona que entiende de Criptoanálisis. Mensaje: "Eu fq sgefa qx mdfuogxa za fq axhupqe pq pqvmd gz oayqzfmdua mx ruzmx pq xm qzfdmpm"
La fuerza bruta es la técnica de intentar agregar todas las llaves posibles para tratar de descifrar el mensaje. Debido a que solo hay 26 combinaciones posibles, descifrar este mensaje para una persona idonea en el tema sería muy fácil.

Ahora vamos añadir alguna porción de código a nuestras funciones para forzar descifrar el mensaje:

def getOpcion():

    while True:
        print '¿Tu quieres encriptar, desencriptar o hacer fuerza bruta a un mensaje?'
        opcion = raw_input().lower()
        if opcion in 'encriptar e desencriptar d bruta b'.split():
            return opcion
        else:
            print 'Escribir "encriptar", "e" , "desencriptar", "d", "bruta" o "b".'

Y en el final del código, cuando llamamos a las funciones agregamos lo siguiente:

opcion = getOpcion()
mensaje = getMensaje()
if opcion[0] != 'b':
    llave = getLlave()

print('El texto traducido es:')
if opcion[0] != 'b':
    print(getTraducirMensaje(opcion, mensaje, llave))
else:
    for llave in range(1, LONG_LLAVE + 1):
        print(llave, getTraducirMensaje('desencriptar', mensaje, llave))

El resultado de la fuerza bruta se ve en la siguiente imágen:

Fuerza Bruta para desencriptar Mensaje
Fuerza Bruta para desencriptar Mensaje

Podemos ver que la llave 12 es la que descifra el mensaje. Esta fuerza bruta hubiera sido dificil de hacer en los tiempos de Cesar, pero hoy en día esto se hace de manera fácil incluso para millones de llaves.

Saludos, y como dice el mensaje descifrado: Sus comentarios motivan a seguir investigando 😉

  1. Unknown dice:

    Muy útil y didáctico el ejemplo que has expuesto, para gente que como yo nos estamos iniciando en el mundo de la programación y además con Python. Te animo a que publiques más artículos como éste e incrementando la dificultad paulatinamente. Muchas gracias!!

    1. PythonDiario dice:

      Hola Vicente, gracias por tu comentario. Seguiremos agregando más artículos de este estilo. Saludos

  2. rctorr dice:

    Excelente exposición acerca del cifrado y hechando mano de python como herramienta! Tal ves si dejaran un texto cifrado al final sería un buen reto, digo sólo tal ves 😉 Salu2+

    1. PythonDiario dice:

      Gracias Ricardo por visitar el blog y tu recomendación. Saludos

  3. Unknown dice:

    Hola, me sale este error:
    Traceback (most recent call last):
    File "python", line 13
    print '¿Tu quieres encriptar o desencriptar un mensaje?'
    ^
    SyntaxError: Missing parentheses in call to 'print'

    Me podrías decir que tengo que hacer?
    Gracias.

    1. rctorr dice:

      Hola Unknown!

      Muy posiblemente ese error sea porque estás usando Python versión 3, lo que puedes verificar ejecutando el comando: python --version. En la versión 3 de python la instrucción print se escribe usando paréntesis, por ejemplo print('Hola pythondiario').

      Y aunque Diego no lo menciona, el está usando python versión 2.7. Así que tienes dos opciones, si deseas usar el código tal cual, entonces instalar Python 2.7, de lo contrario cambia todos los print para que usen paréntesis.

      Salu2+ y espero se resuelva tu error.

    2. PythonDiario dice:

      Excelente respuesta Ricardo 😉

      Saludos!!!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir
White Monkey