Ir al contenido principal

Criptografía (CCXLVII): Cifrado de Playfair en python

Continúo poniendo scripts de programación en python para automatizar tareas que tengan relación con la criptografía.

En este post incluyo y comento un script en python para el cifrado y descifrado utilizando el cifrado de Playfair.

Antes de poner el script, comentar cómo se cifraba y descifraba manualmente.

El cifrado de Playfair consistía en utilizar una matriz de 5x5 caracteres que se iba rellenando, de izquierda a derecha y de arriba a abajo, con las letras de la clave (eliminando las posibles letras repetidas de la misma) y se completaba con el resto de las letras del alfabeto (las que no figurasen en la clave), excluidas la "J" y la "Ñ".

Por ejemplo, utilizando la palabra "PLAYFAIR" como clave, obtendríamos la matriz que se muestra a continuación:

Cifrar:

Para cifrar, para cada par de caracteres, ya que en este criptosistema se realiza una sustitución digrámica, se actuaba de la siguiente forma:

- Si ambos caracteres estaban situados en la misma fila cada uno de ellos se sustituía por el carácter situado inmediatamente a su derecha (si el carácter a sustituir era el último de la fila se sustituía por el primero de la fila).

Si ambos caracteres estaban situados en la misma columna cada uno de ellos se sustituía por el carácter situado inmediatamente debajo de él (si el carácter a sustituir era el último de la columna se sustituía por el primero de la columna).

- Si ambos caracteres estaban situados en diferente fila y diferente columna, la primera letra se sustituía por aquella situada en su misma fila y la misma columna que la segunda, y la segunda por aquella situada en su misma fila y la misma columna que la primera.

En el script el cifrado se implementa de la siguiente manera:

- Se simula la matriz de 5x5 mediante una cadena de caracteres a la que se van agregando, en primer lugar, los caracteres de la clave, sin incluir los posibles caracteres repetidos en la misma, y, posteriormente, el resto de caracteres del alfabeto. Todo ello, mediante los dos siguientes bucles:

# Agregar a una cadena los caracteres de la clave no repetidos.
    matriz=''
    for caracter in clave:
        if caracter not in matriz:
            matriz=matriz+caracter
# Agregar a la cadena el resto de caracteres del alfabeto.
    for caracter in alfabeto:
        if caracter not in matriz:
            matriz=matriz+caracter

El alfabeto empleado es: "ABCDEFGHIKLMNOPQRSTUVWXYZ" (nótese que no figuran la letra "J", que compartiría posición con la "I", ni la "Ñ"), por lo que se obtendrá una cadena de 25 caracteres de longitud que simula la matriz de 5 filas x 5 columnas.

En el ejemplo puesto anteriormente:

"PLAYFIRBCDEGHKMNOQSTUVWXZ"

- Las parejas de caracteres que se cifrarán serán los pares de caracteres consecutivos del texto plano, pero si algún par está formado por letras iguales hay que incluir un carácter de relleno entre ambas (por ejemplo: "X", que es una letra de baja frecuencia). Al finalizar lo anterior, si la cadena resultado tiene una longitud impar hay que añadirle el carácter de relleno (en nuestro caso "X") al final, ya que como vamos a realizar una sustitución digrámica (de grupos de dos caracteres), lógicamente, la longitud de la cadena debe ser par:

Esto se implementa en el script de la siguiente manera:

len(): la función len() devuelve el número de items de un objeto. Cuando el objeto es una cadena devuelve el número de caracteres de la misma.

En el script devuelve el número de caracteres de la cadena con el texto plano y, cuando finaliza de incluir caracteres de relleno, la de la cadena resultado de añadir éstos. En el ejemplo que veremos a continuación 23 y 31, respectivamente. Nótese que la última longitud indica será justo después de incluir los caracteres de relleno, y como es impar se añadirá una última "X" a la cadena resultado.

%: el operador % realiza la división modular entre dos operandos, es decir, obtiene el resto de dividir el operando 1 entre el operando 2.

parejas=''
i=0
while i < len(texto):
    if i != len(texto)-1:
        if texto[i]==texto[i+1]:
            parejas+=texto[i]+'X'
            i-=1
        else:
            parejas+=texto[i]+texto[i+1]
    else:
        parejas+=texto[i]
    i+=2
if len(parejas)%2!=0:
    parejas+='X'

Para ver cómo se van incluyendo los caracteres de relleno, supongamos que la cadena con el texto en claro cifrar es:

"AABBCBCCCDDEFFFFEEGGGHH",

por lo que se obtendría la siguiente cadena:

"AXABBXBCCXCXCDDEFXFXFXFEEGGXGHHX"

- Y, finalmente y conforme a la matriz con la clave, se cifran los pares de letras consecutivos de la cadena obtenida en el paso anterior, en función de que los caracteres que conforman cada par estén en la misma fila de la matriz, estén en la misma columna o se sitúen en distinta fila y distinta columna.

En el script:

//: el operador // obtiene el cociente entero de dividir el operando 1 entre el operando 2.

matriz.find(parejas[i])//5: el método find() encuentra la primera ocurrencia del valor especificado en una cadena de caracteres y devuelve la posición que ésta ocupa. La primera posición es 0 y si no se encuentra el valor devuelve -1.

Para cada par de caracteres a cifrar:

Si las letras están en la misma fila en la matriz con la clave, es decir, si el cociente entero de dividir las sendas posiciones que ocupan las letras de la pareja entre 5 son iguales (if matriz.find(parejas[i])//5==matriz.find(parejas[i+1])//5:) bastará con sumar 1 a sus respectivas posiciones en la matriz de la clave para obtener las correspondientes letras por la que será sustituida cada una de ellas, considerando que si alguna de ellas se encuentra en la última columna debe ser sustituida por la primera letra de la fila en la que se encuentre:

if matriz.find(parejas[i])%5!=4:
    criptograma=criptograma+matriz[matriz.find(parejas[i])+1]
else:
    criptograma=criptograma+matriz[matriz.find(parejas[i])-4]
if matriz.find(parejas[i+1])%5!=4:
    criptograma=criptograma+matriz[matriz.find(parejas[i+1])+1]
else:
    criptograma=criptograma+matriz[matriz.find(parejas[i+1])-4]

En el ejemplo que hemos puesto anteriormente, para la cuarta pareja "BC": 7//5 = 1 es igual que 8//5 = 1, por lo que ambas letras están en la misma fila; la "B" sería sustituida por "C" y "C" por "D".

Si las letras están en la misma columna en la matriz con la clave, es decir: si el resto de dividir las  sendas posiciones que ocupan las letras de la pareja entre 5 son iguales (if matriz.find(parejas[i])%5==matriz.find(parejas[i+1])%5:) bastará con sumar 5 a sus respectivas posiciones en la matriz de la clave para obtener las correspondientes letras por la que será sustituida cada una de ellas, considerando que si alguna de ellas se encuentra en la última fila debe ser sustituida por la primera letra de la columna en la que se encuentre:

criptograma=criptograma+matriz[(matriz.find(parejas[i])+5)%25]
criptograma=criptograma+matriz[(matriz.find(parejas[i+1])+5)%25]

En el ejemplo que hemos puesto anteriormente, para la segunda pareja "AB": 2%5 = 2 es igual que 7%5 = 2, por lo que ambas letras están en la misma columna; l"A" sería sustituida por "B" y "B" por "H".

Por el contrario, si las letras están en distinta fila y distinta columna en la matriz con la clave, es decir, si no se ha satisfecho ninguna de las condiciones anteriores, bastará con obtener la distancia entre las columnas en las que están situadas ambas letras (columna segunda letra de la pareja - columna primera letra de la pareja) y sumar dicha distancia a la posición en la que se encuentra la primera de ellas para obtener la letra que la sustituirá, y restar dicha distancia a la posición en la que se encuentra la segunda de ellas para obtener la letra que la sustituirá.

distancia_columnas=matriz.find(parejas[i+1])%5-matriz.find(parejas[i])%5          criptograma=criptograma+matriz[matriz.find(parejas[i])+distancia_columnas]+matriz[matriz.find(parejas[i+1])-distancia_columnas]

En el ejemplo que hemos puesto anteriormente, las letras de la primera pareja, "AX",  están en diferente fila y diferente columna, y la distancia entre ambas columnas es: 3 - 2 = 1, por lo que la letra "A" se sustituirá por "Y" (2 + 1 = 3) y la "X" por "W" (23 - 1 = 22).

Descifrar:

Para descifrar, partiendo del criptograma, se aplican las mismas reglas del cifrado teniendo en cuenta que si una letra del criptograma se obtuvo por ser la siguiente a la del texto plano en la tabla de la matriz, ahora la letra del texto plano será la anterior a la del criptograma, etc.

Script python del cifrado de Playfair:

El script es el siguiente: 

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

# CIFRADO DE PLAYFAIR
#
# Cifra y descifra textos en claro y criptogramas, respectivamente,
# utilizando el cifrado de Playfair.
#
# http://mikelgarcialarragan.blogspot.com/

import re
from unicodedata import normalize

# ALFABETO:
alfabeto = "ABCDEFGHIKLMNOPQRSTUVWXYZ"

# FUNCIÓN DE CIFRADO:
def cifrar(texto_claro, clave):
    criptograma = ''
    matriz=crear_matriz(clave)
    parejas=establecer_parejas(texto_claro)
    for i in range(0,len(parejas)-1,2):
        if matriz.find(parejas[i])//5==matriz.find(parejas[i+1])//5:
            # Los caracteres de la pareja están en la misma fila.
            if matriz.find(parejas[i])%5!=4:
                criptograma=criptograma+matriz[matriz.find(parejas[i])+1]
            else:
                criptograma=criptograma+matriz[matriz.find(parejas[i])-4]
            if matriz.find(parejas[i+1])%5!=4:
                criptograma=criptograma+matriz[matriz.find(parejas[i+1])+1]
            else:
                criptograma=criptograma+matriz[matriz.find(parejas[i+1])-4]
        elif matriz.find(parejas[i])%5==matriz.find(parejas[i+1])%5:
            # Los caracteres de la pareja están en la misma columna.
            criptograma=criptograma+matriz[(matriz.find(parejas[i])+5)%25]
            criptograma=criptograma+matriz[(matriz.find(parejas[i+1])+5)%25]
        else:
            # Los caracteres de la pareja están en distinta fila y distinta columna.
            distancia_columnas=matriz.find(parejas[i+1])%5-matriz.find(parejas[i])%5
            criptograma=criptograma+matriz[matriz.find(parejas[i])+distancia_columnas]+matriz[matriz.find(parejas[i+1])-distancia_columnas]
    return criptograma

# FUNCIÓN DE DESCIFRADO:
def descifrar(criptograma, clave):
    texto_claro = ''
    matriz=crear_matriz(clave)
    parejas=establecer_parejas(criptograma)
    for i in range(0,len(parejas)-1,2):
        if matriz.find(parejas[i])//5==matriz.find(parejas[i+1])//5:
            # Los caracteres de la pareja están en la misma fila.
            if matriz.find(parejas[i])%5!=0:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i])-1]
            else:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i])+4]
            if matriz.find(parejas[i+1])%5!=0:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i+1])-1]
            else:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i+1])+4]
        elif matriz.find(parejas[i])%5==matriz.find(parejas[i+1])%5:
            # Los caracteres de la pareja están en la misma columna.
            if matriz.find(parejas[i])//5!=0:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i])-5]
            else:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i])+20]
            if matriz.find(parejas[i+1])//5!=0:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i+1])-5]
            else:
                texto_claro=texto_claro+matriz[matriz.find(parejas[i+1])+20]
        else:
            # Los caracteres de la pareja están en distinta fila y distinta columna.
            distancia_columnas=matriz.find(parejas[i+1])%5-matriz.find(parejas[i])%5
            texto_claro=texto_claro+matriz[matriz.find(parejas[i])+distancia_columnas]+matriz[matriz.find(parejas[i+1])-distancia_columnas]
    return texto_claro

# CREAR UNA CADENA CON LOS CARACTERES DE LA MATRIZ DE 5x5 UTILIZANDO LA CLAVE:
def crear_matriz(clave):
# Agregar a una cadena los caracteres de la clave no repetidos.
    matriz=''
    for caracter in clave:
        if caracter not in matriz:
            matriz=matriz+caracter
# Agregar a la cadena el resto de caracteres del alfabeto.
    for caracter in alfabeto:
        if caracter not in matriz:
            matriz=matriz+caracter
    return matriz

# ESTABLECER PAREJAS DE CARACTERES:
def establecer_parejas(texto):
    parejas=''
    i=0
    while i < len(texto):
        if i != len(texto)-1:
            if texto[i]==texto[i+1]:
                parejas+=texto[i]+'X'
                i-=1
            else:
                parejas+=texto[i]+texto[i+1]
        else:
            parejas+=texto[i]
        i+=2
    if len(parejas)%2!=0:
        parejas+='X'
    return parejas

# MENÚ:
# Se presenta el menú para que se seleccione una opción.
def main():
    salir = False
    while not salir:
        print ("")
        print ("*** MENÚ *****************************************")
        print ("1. Cifrar.")
        print ("2. Descifrar.")
        print ("3. Salir.")
        print ("")
        opcion = input("Por favor, seleccione una opción: ")
        if opcion == "1":
            print ("")
            print ("--- CIFRAR:")
            # Cifrar: Se introducen el texto en claro y la clave. Se convierten los caracteres a mayúsculas,
            # el carácter "J" se sustituye por "I" y la letra "Ñ" por "N", y se eliminan de ambos los espacios,
            # las tildes, diéresis, etc.
            texto_claro = clave = "*"
            while not texto_claro.isalpha():
                texto_claro = input('Texto en claro a cifrar: ').upper()
                texto_claro = texto_claro.replace('J','I')
                texto_claro = texto_claro.replace('Ñ','N')
                texto_claro = texto_claro.replace(' ','')
                texto_claro = re.sub(r"([^n\u0300-\u036f]|n(?!\u0303(?![\u0300-\u036f])))[\u0300-\u036f]+",
                                     r"\1", normalize("NFD", texto_claro), 0, re.I)
                texto_claro = normalize("NFC", texto_claro)
                if texto_claro.isalpha():
                    print ("[+] Texto en claro a cifrar:", texto_claro)
                    while not clave.isalpha():
                        clave = input('Clave: ').upper()
                        clave = clave.replace('J','I')
                        clave = clave.replace('Ñ','N')
                        clave = clave.replace(' ','')
                        clave = re.sub(r"([^n\u0300-\u036f]|n(?!\u0303(?![\u0300-\u036f])))[\u0300-\u036f]+",
                                    r"\1", normalize("NFD", clave), 0, re.I)
                        clave = normalize("NFC", clave)
                        if clave.isalpha():
                            print ("[+] Clave:", clave)
                            criptograma = cifrar(texto_claro, clave)
                            print ("[+] Criptograma:", criptograma)
                        else:
                            print ("*** ERROR: La clave sólo debe contener caracteres alfabéticos.")
                else:
                    print ("*** ERROR: El texto en claro a cifrar sólo debe contener caracteres alfabéticos.")
        elif opcion == "2":
            print ("")
            print ("--- DESCIFRAR:")
            # Descifrar: Se introducen el criptograma y la clave. Se convierten los caracteres a mayúsculas,
            # el carácter "J" se sustituye por "I" y la letra "Ñ" por "N", y se eliminan de ambos los espacios,
            # las tildes, diéresis, etc.
            criptograma = clave = "*"
            while not criptograma.isalpha():
                criptograma = input('Criptograma a descifrar: ').upper()
                criptograma = criptograma.replace('J','I')
                criptograma = criptograma.replace('Ñ','N')
                criptograma = criptograma.replace(' ','')
                criptograma = re.sub(r"([^n\u0300-\u036f]|n(?!\u0303(?![\u0300-\u036f])))[\u0300-\u036f]+",
                                      r"\1", normalize("NFD", criptograma), 0, re.I)
                criptograma = normalize("NFC", criptograma)
                if criptograma.isalpha():
                    print ("[+] Criptograma a descifrar:", criptograma)
                    while not clave.isalpha():
                        clave = input('Clave: ').upper()
                        clave = clave.replace('J','I')
                        clave = clave.replace('Ñ','N')
                        clave = clave.replace(' ','')
                        clave = re.sub(r"([^n\u0300-\u036f]|n(?!\u0303(?![\u0300-\u036f])))[\u0300-\u036f]+",
                                    r"\1", normalize("NFD", clave), 0, re.I)
                        clave = normalize("NFC", clave)
                        if clave.isalpha():
                            print ("[+] Clave:", clave)
                            texto_claro = descifrar(criptograma, clave)
                            print ("[+] Texto en claro:", texto_claro)
                        else:
                            print ("*** ERROR: La clave sólo debe contener caracteres alfabéticos.")
                else:
                    print ("*** ERROR: El criptograma a descifrar sólo debe contener caracteres alfabéticos.")
        elif opcion == "3":
            print ("*** FIN ******************************************")
            salir = True
        else:
            print ("*** ERROR: Opción no válida.")
	
if __name__ == '__main__':
    main()

Lo ejecuto:

Cifrar:

Nótese que el script ha sustituido el carácter "j" que se ha introducido en el texto en claro a cifrar por "I", ya que "I" y "J" comparten la misma posición en la matriz con la clave. Además, si se hubiera introducido la letra "Ñ" el script la habría sustituido por "N".

Descifrar:

Comentarios

Entradas populares de este blog

Criptografía (I): cifrado Vigenère y criptoanálisis Kasiski

Hace unos días mi amigo Iñaki Regidor ( @Inaki_Regidor ), a quien dedico esta entrada :), compartió en las redes sociales un post titulado "Criptografía: el arte de esconder mensajes"  publicado en uno de los blogs de EiTB . En ese post se explican ciertos métodos clásicos para cifrar mensajes , entre ellos el cifrado de Vigenère , y , al final del mismo, se propone un reto consistente en descifrar un mensaje , lo que me ha animado a escribir este post sobre el método Kasiski  para atacar un cifrado polialfabético ( conociendo la clave descifrar el mensaje es muy fácil, pero lo que contaré en este post es la forma de hacerlo sin saberla ). El mensaje a descifrar es el siguiente: LNUDVMUYRMUDVLLPXAFZUEFAIOVWVMUOVMUEVMUEZCUDVSYWCIVCFGUCUNYCGALLGRCYTIJTRNNPJQOPJEMZITYLIAYYKRYEFDUDCAMAVRMZEAMBLEXPJCCQIEHPJTYXVNMLAEZTIMUOFRUFC Como ya he dicho el método de Vigenère es un sistema de sustitución polialfabético , lo que significa que, al contrario que en un sistema de

Criptografía (XXIII): cifrado de Hill (I)

En este post me propongo explicar de forma comprensible lo que he entendido sobre el cifrado de Hill , propuesto por el matemático Lester S. Hill , en 1929, y que se basa en emplear una matriz como clave  para cifrar un texto en claro y su inversa para descifrar el criptograma correspondiente . Hay tres cosas que me gustan de la criptografía clásica, además de que considero que ésta es muy didáctica a la hora de comprender los sistemas criptográficos modernos: la primera de ellas es que me "obliga" a repasar conceptos de matemáticas aprendidos hace mucho tiempo y, desgraciadamente, olvidados también hace demasiado tiempo, y, por consiguiente, que, como dice  Dani , amigo y coautor de este blog, me "obliga" a hacer "gimnasia mental"; la segunda es que, en la mayoría de las ocasiones, pueden cifrarse y descifrase los mensajes, e incluso realizarse el criptoanálisis de los criptogramas, sin más que un simple lápiz y papel, es decir, para mi es como un pasat

¿Qué significa el emblema de la profesión informática? (I)

Todas o muchas profesiones tienen un emblema que las representa simbólicamente y en el caso de la  informática: " es el establecido en la resolución de 11 de noviembre de 1977  para las titulaciones universitarias superiores de informática, y  está constituido por una figura representando en su parte central  un  núcleo toroidal de ferrita , atravesado por  hilos de lectura,  escritura e inhibición . El núcleo está rodeado por  dos ramas : una  de  laurel , como símbolo de recompensa, y la otra, de  olivo , como  símbolo de sabiduría. La  corona  será la  de la casa real  española,  y bajo el escudo se inscribirá el acrónimo de la organización. ". Veamos los diferentes elementos tomando como ejemplo el emblema del COIIE/EIIEO (Colegio Oficial de Ingenieros en Informática del País Vasco/ Euskadiko Informatikako Ingeniarien Elkargo Ofiziala ) . Pero no sólo el COIIE/EIIEO adopta el emblema establecido en dicha resolución, sino que éste se adopta también como im