Ir al contenido principal

Criptografía (CCXLV): Cifrado por transposición columnar simple 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 por transposición columnar simple.

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

El cifrado por transposición columnar simple se basa en una tabla o matriz cuyo número de columnas viene determinado por el tamaño o longitud de la clave de cifrado.

Para cifrar: el texto en claro se disponía en dicha tabla o matriz, desde la fila situada más arriba hasta la de más abajo, y dentro de cada una de ellas desde la columna ubicada más a la izquierda hasta la de más a la derecha, un carácter debajo de cada letra de la clave. Posteriormente, se ordenaban las columnas conforme al orden alfabético de las letras de la clave (si hay letras repetidas el orden se establecía de izquierda a derecha: primero la columna de una misma letra situada más a la izquierda, después la siguiente de esa misma letra más a la izquierda y así sucesivamente) y, finalmente, se leía el resultado obtenido por columnas. 

Pongo como ejemplo el cifrado del mensaje que veremos más adelante al ejecutar el script:

De esta forma, en el cifrado del texto en claro  "EJEMPLOCIFRADO" se obtendría el criptograma: "ECDELRPFJOAMIO".

Y para descifrar un criptograma simplemente se actuaba de forma inversa:

En el script, las funciones de cifrado y descifrado se implementan de la siguiente manera:

Cifrar:

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 del texto a cifrar y de la clave, en el ejemplo 14 y 5, respectivamente.

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

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

if len(texto)%len(clave)==0:
      filas=len(texto)//len(clave)
  else:
      filas=len(texto)//len(clave)+1

Calcula el número de filas de la tabla o matriz:

Si el resto de dividir la longitud del texto a cifrar entre la longitud de la clave es 0: el número de filas es el cociente de dicha división.

En caso contrario: el número de filas es el cociente entero de dicha división + 1.  

En el ejemplo: como resto(longitud texto plano / longitud clave) = resto(14 / 5) = 4, entonces número de filas = cociente(longitud texto plano / longitud clave) + 1 = cociente(14 / 5) + 1 = 2 + 1 = 3

- texto_claro=[" "]*filas*len(clave)
  for i in range(0,len(texto)):
      texto_claro[i]=texto[i]

Inicializa con espacios una lista en la que se va a incluir el texto en claro a cifrar. Tendrá tantos elementos como número de filas * número de columnas (longitud clave), y se incluyen en ella los caracteres del texto en claro.

En el ejemplo:

['E', 'J', 'E', 'M', 'P', 'L', 'O', 'C', 'I', 'F', 'R', 'A', 'D', 'O', ' ']

- clave_alfabetica_ordenada=sorted(clave)ordena alfabéticamente los caracteres de la clave y deja el resultado en una lista.

En el ejemplo:

['A', 'C', 'E', 'L', 'V']

- clave_numerica=[]
  for i in range(0,len(clave)):
      clave_numerica.append(clave_alfabetica_ordenada.index(clave[i]))

Asigna un número a cada carácter de la clave en función de la posición que ocupa cada uno de ellos en la clave ordenada alfabéticamente.

En el ejemplo:

[1, 3, 0, 4, 2]

- for i in range(1,len(clave)):
      for j in range(0,i):
          if clave_numerica[i]==clave_numerica[j]:
              clave_numerica[i]+=1

Asigna un valor ascendente de +1 al segundo y siguientes caracteres repetidos de una misma letra de la clave.

En el ejemplo (no hay caracteres repetidos en la clave):

[1, 3, 0, 4, 2]

- Y finalmente, el criptograma se obtiene mediante los dos siguientes bucles anidados:

  for i in range(0,len(clave)):
      for j in range(0,filas):
          criptograma+=texto_claro[clave_numerica.index(i)+len(clave)*j]

En el ejemplo, el criptograma (del que se eliminan los espacios en blanco o "huecos") es:

"ECDELRPFJOAMIO".

Descifrar:

Se implementa actuando de forma inversa a la del cifrado:

- if len(texto)%len(clave)==0:
      filas=len(texto)//len(clave)
      columnas_completas=len(clave)
  else:
      filas=len(texto)//len(clave)+1
      columnas_completas=len(texto)%len(clave)
  columnas_incompletas=len(clave)-columnas_completas

De forma análoga que en el cifrado, se calcula el número de filas de la tabla o matriz, pero, además, se calculan también el número de columnas completas (sin espacios en su última fila o sin "huecos") que se produjeron en el proceso de cifrado del texto en claro, y, como consecuencia, el número de columnas incompletas (con espacios en su última fila o con "huecos") que se produjeron en dicho proceso, que es igual a la longitud de la clave menos el número de columnas completas.

Si el resto de dividir la longitud del criptograma a descifrar entre la longitud de la clave es 0: el número de filas es el cociente de dicha división y todas las columnas están completas, es decir, el número de columnas completas es igual que la longitud de la clave.

En caso contrario: el número de filas es el cociente entero de dicha división + 1 y el número de columnas completas es el resto de dividir la longitud del criptograma entre la longitud de la clave.

El número de columnas incompletas es igual a la longitud de la clave menos el número de columnas completas.

En el ejemplo: como resto(longitud criptograma / longitud clave) = resto(14 / 5) = 4, entonces número de filas = cociente(longitud criptograma / longitud clave) + 1 = cociente(14 / 5) + 1 = 2 + 1 = 3 y el número de columnas completas es el resto de dicha división: 4, y, como consecuencia, el número de columnas incompletas es igual a: longitud clave - número de columnas completas = 5 - 4 = 1.

clave_alfabetica_ordenada=sorted(clave): ordena alfabéticamente los caracteres de la clave y deja el resultado en una lista.

En el ejemplo:

['A', 'C', 'E', 'L', 'V']

clave_numerica=[]
  for i in range(0,len(clave)):
      clave_numerica.append(clave_alfabetica_ordenada.index(clave[i]))

Asigna un número a cada carácter de la clave en función de la posición que ocupa cada uno de ellos en la clave ordenada alfabéticamente.

En el ejemplo:

[1, 3, 0, 4, 2]

for i in range(1,len(clave)):
      for j in range(0,i):
          if clave_numerica[i]==clave_numerica[j]:
              clave_numerica[i]+=1

Asigna un valor ascendente de +1 al segundo y siguientes caracteres repetidos de una misma letra de la clave.

En el ejemplo (no hay caracteres repetidos en la clave):

[1, 3, 0, 4, 2]

- huecos=[]
  for i in range(-1,-columnas_incompletas-1,-1):
      huecos.append((clave_numerica[i]*filas)+(filas-1))
  huecos=sorted(huecos)

Calcula las posiciones del criptograma en las que se produjeron espacios o "huecos" en el proceso de cifrado y clasifica ascendentemente dichas posiciones.

En el ejemplo: en el proceso de cifrado sólo se produjo un hueco y éste se encuentra en la posición 8 del criptograma.

- texto_huecos=''
  huecos_incluidos=0
  for i in range(0,len(texto)):
      if i in huecos:
          texto_huecos+=' '
          huecos_incluidos+=1
          if columnas_incompletas-huecos_incluidos!=0:
              huecos[huecos_incluidos]=huecos[huecos_incluidos]-huecos_incluidos
      texto_huecos+=texto[i]

Incluye un espacio en cada posición con "hueco" del criptograma.

En el ejemplo:

"ECDELRPF JOAMIO"

- criptograma=[" "]*filas*len(clave)
  for i in range(0,len(texto_huecos)):
          criptograma[((i%filas)*len(clave))+(i//filas)]=texto_huecos[i]

Inicializa con espacios la lista en la que se va a incluir el criptograma a descifrar. Tendrá tantos elementos como número de filas * número de columnas (longitud clave), y se incluyen en ella los caracteres del criptograma

En el ejemplo:

['E', 'E', 'P', 'J', 'M', 'C', 'L', 'F', 'O', 'I', 'D', 'R', ' ', 'A', 'O']

- Y finalmente, el texto en claro se obtiene mediante los dos siguientes bucles anidados:

  for i in range(0,filas):
      for j in range(0,len(clave)):
          texto_claro+=criptograma[clave_numerica[j]+len(clave)*i]

En el ejemplo, el texto en claro es:

"EJEMPLOCIFRADO"

Script python del cifrado por transposición columnar simple:

El script es el siguiente:

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

# CIFRADO POR TRANSPOSICIÓN COLUMNAR SIMPLE:
#
# Cifra y descifra textos en claro y criptogramas, respectivamente,
# utilizando la transposición columnar simple.
#
# http://mikelgarcialarragan.blogspot.com/

import re
from unicodedata import normalize

# FUNCIÓN DE CIFRADO:
def cifrar(texto, clave):
    criptograma=''
    if len(texto)%len(clave)==0:
        filas=len(texto)//len(clave)
    else:
        filas=len(texto)//len(clave)+1
    texto_claro=[" "]*filas*len(clave)
    for i in range(0,len(texto)):
        texto_claro[i]=texto[i]
    clave_alfabetica_ordenada=sorted(clave)
    clave_numerica=[]
    for i in range(0,len(clave)):
         clave_numerica.append(clave_alfabetica_ordenada.index(clave[i]))
    for i in range(1,len(clave)):
        for j in range(0,i):
            if clave_numerica[i]==clave_numerica[j]:
                clave_numerica[i]+=1
    for i in range(0,len(clave)):
        for j in range(0,filas):
            criptograma+=texto_claro[clave_numerica.index(i)+len(clave)*j]
    return criptograma.replace(' ','')

# FUNCIÓN DE DESCIFRADO:
def descifrar(texto, clave):
    texto_claro=''
    if len(texto)%len(clave)==0:
        filas=len(texto)//len(clave)
        columnas_completas=len(clave)
    else:
        filas=len(texto)//len(clave)+1
        columnas_completas=len(texto)%len(clave)
    columnas_incompletas=len(clave)-columnas_completas
    clave_alfabetica_ordenada=sorted(clave)
    clave_numerica=[]
    for i in range(0,len(clave)):
         clave_numerica.append(clave_alfabetica_ordenada.index(clave[i]))
    for i in range(1,len(clave)):
        for j in range(0,i):
            if clave_numerica[i]==clave_numerica[j]:
                clave_numerica[i]+=1
    huecos=[]
    for i in range(-1,-columnas_incompletas-1,-1):
        huecos.append((clave_numerica[i]*filas)+(filas-1))
    huecos=sorted(huecos)
    texto_huecos=''
    huecos_incluidos=0
    for i in range(0,len(texto)):
        if i in huecos:
            texto_huecos+=' '
            huecos_incluidos+=1
            if columnas_incompletas-huecos_incluidos!=0:
                huecos[huecos_incluidos]=huecos[huecos_incluidos]-huecos_incluidos
        texto_huecos+=texto[i]
    criptograma=[" "]*filas*len(clave)
    for i in range(0,len(texto_huecos)):
            criptograma[((i%filas)*len(clave))+(i//filas)]=texto_huecos[i]
    for i in range(0,filas):
        for j in range(0,len(clave)):
            texto_claro+=criptograma[clave_numerica[j]+len(clave)*i]
    return texto_claro.replace(' ','')

# 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 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(' ','')
                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(' ','')
                        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 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(' ','')
                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(' ','')
                        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:

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