miércoles, 29 de julio de 2020

Criptografía en Python - Aumentando la seguridad, trucos y aplicativos en la vida real



Aplicativo real del cifrado en Internet


Ya hemos presenciado tanto el cifrado simétrico como el asimétrico, inclusive, una combinación de éstos, como lo es el cifrado híbrido, pero hoy vamos a aprender cómo mantener una infraestructura sencilla y hablar un poco sobre cosas relevantes en cuanto a la seguridad, asimismo, realizaremos un pequeño programa que haga uso de todo lo que hemos comprendido a los largo de estas partes.

La entropía


La entropía es la aletoriedad que obtiene a través de un sistema operativo o una aplicación para su uso en los algoritmos o calculos que requieran datos aleatorios.


Haciendo mención al cifrado híbrido, como ya sabemos, en el cifrado híbrido se utiliza el cifrado simétrico para cifrar los datos y se utiliza el cifrado asimétrico para cifrar la clave (también llamada clave de sesión), clave que debería ser aleatoria y es la base para proteger los datos.

PRNG y CSPRNG


Los generadores de números pseudo-aleatorios (pseudo-random number generators, o simplemente PRNG, en inglés) y los generadores de números pseudo-aleatorios criptográficamente seguros (cryptography secure random number generators, o simplemente CSPRNG, en inglés) son los que permiten incluir cierto grado de aletoriedad a los algoritmos (o en el caso mencionado, del cifrado híbrido).


La diferencia relevante entre cada uno, es que los PRNG son inseguros cuando se les aplica a la criptografía, mientras que los CSPRNG son por definición PRNG con propiedades que lo hacen adecuados para la criptografía.


Imaginen que están creando una página web, al usuario se le proporciona un identificador que aparentemente es único, luego de que unos miles de usuarios se han registrado, llega la pesadilla, un usuario tiene el identificador de otro usuario e imaginense que esa página web sea una aplicación que maneje dinero...

random vs. secrets

En la parte anterior se usó os.urandom(...) para generar una clave aleatoria, pero a diferencia de secrets no proporciona comodamente una interfaz para la creación de aplicaciones (como de escritorio, web y demás).


Un enlace para registrar muy trivial


Aunque no lo parezca, es muy trivial encontrar una URL en nuestra bandeja de correo electrónico al registrarnos en algún servicio que requiera la confirmación explícita del mismísmo usuario. Como ya mencioné anteriormente, a diferencia de os.urandom(...) y secrets, es que éste proporciona una interfaz más comoda para este tipo de aplicativos.


Siguiendo con las comparaciones, hay que aclarar que no se debe ni pensar ni usar la librería random para la criptografía, sino más bien para el modelado de datos y la simulación.

Creando una infraestructura


Ya hemos aprendido a cómo usar RSA en python con la librería pycryptodome, aunque hay algo faltante que hasta puede ser una limitante en la usabilidad referente al usuario, la importación de claves.


No hace falta mencionar los formatos admitidos que se ha hablado, pero lo que debo mencionar es lo fácil que es importar así como su contraparte, la exportación.


Para este ejemplo usaremos el código de la parte anterior y le agregaremos un método más, llamado '.import_key(...)' con el objetivo de importar las claves en los formatos OpenSSH, PEM o DER.

from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

def generate(bit_size):
    keys = RSA.generate(bit_size)

    return (keys, keys.publickey())

def encrypt(pub_key, data):
    cipher = PKCS1_OAEP.new(pub_key)

    return cipher.encrypt(data)

def decrypt(priv_key, data):
    cipher = PKCS1_OAEP.new(priv_key)

    return cipher.decrypt(data)

def import_key(filename, passphrase=None):
    with open(filename, 'rb') as fd:
        return RSA.import_key(fd.read(), passphrase)


Como se puede observar, solo agregamos un método más para importar claves RSA. Aquí está la observación empírica:


Guardando el par de claves en el disco

Leyendo las claves almacenadas en el disco con la aplicación cat

Importación realizada con éxito


Lo que acabamos de ver es más que una simple utilidad, es algo que no puede faltar en una aplicación de esta índole, de ser así el usuario tendría que estar generando un par de claves sin sentido o sólo se pudieran usar mientras estén en memoria, pero nada más. Además que así se puede compartir fácilmente la clave pública a nuestro destinatario.

Exportando e importando con una contraseña


Tal vez exportar e importar una clave y guardarla en el disco no suene muy seguro (especialmente para la clave privada), quizá lo que el usuario desee es cifrar el par de claves o necesitamos hacerlo. pycryptodome nos ofrece algunos parámetros que son fácilmente configurables.


En el caso de la clave pública no es muy necesario y es poco frecuente ya que ésta no pone en peligro la infraestructura ni mucho menos, de hecho es una de las ventajas de la encriptación de clave pública, ya que dos partes se pueden comunicar entre sí.



Exportando la clave privada cifrada con scrypt y AES128-CBC con la contraseña '123'

El parámetro pkcs es necesario especificarlo en 8 cuando se necesite especificar una protección aparte de la que tiene por defecto cuando no se le pasa ningún parámetro, además que también se requiere cuando se ingresa una contraseña (en el parámetro passphrase). Si es una clave pública (o es una clave privada que solo se define el parámetro passphrase), se ignoran los parámetros y se deriva la contraseña con MD5 y Triple DES para el cifrado (todo se considera obsoleto, por lo que es conveniente especificarlos).



Importando la clave privada que está almacenada en el disco

Al tratar de importar sin especificar la contraseña, genera un error, pero cuando se es explícito con ella se descifra y se importa correctamente.

Cifrando archivos


La ventaja del disco de almacenamiento es el tamaño, pero en su contra es muy lenta tanto la lectura como la escritura, se puede resolver con la memoria RAM que es muy rápida pero carece de una limitación en cuanto al tamaño. Una solución no es comprarse una que tenga más capacidad, lo que funcionaría sin problemas es un algoritmo que cifre los datos en partes, por lo se tendría un equilibrio sin considerar el tamaño del mismo.


El truco aquí es crear una versión mejorada de aes_eax.py que tenga implementada lo que se acaba de mencionar (lo de cifrar partes y no todo el archivo por completo), son palabras bonitas pero seguro se verían mejor con código:


import os
import struct

from Crypto.Cipher import AES

# El tamaño de la división del archivo
chunk_size = 1024*64

# La extensión del archivo a la hora
# de encriptarlo.
extension = 'enc'

def encrypt_file(key, filename):
    # Concatenamos el nombre del archivo
    # con el nombre de la extensión.
    output = filename + '.' + extension

    # Obtenemos el tamaño del archivo
    filesize = os.path.getsize(filename)
    
    with open(filename, 'rb') as fd_in:
        with open(output, 'wb') as fd_out:
            # Escribimos el tamaño del archivo.
            #
            # <: Little Indian
            # Q: unsigned long long
            fd_out.write(
                struct.pack('<Q', filesize)

            )

            while (True):
                # Leemos según el tamaño de división
                # proporcionado por el usuario.
                chunk = fd_in.read(chunk_size)

                # Termino de leer, sale
                if (len(chunk) == 0):
                    break

                cipher = AES.new(key, AES.MODE_EAX)
                ciphertext, tag = cipher.encrypt_and_digest(chunk)

                # Escribimos las partes relevantes
                fd_out.write(cipher.nonce)
                fd_out.write(tag)
                fd_out.write(ciphertext)

                # Vaciar el buffer de la memoria y pasarlo al disco
                fd_out.flush()
                # Forzar la escritura de cualquier buffer 
                # (ya sea del sistema operativo o del mismo programa)
                # al disco.
                os.fsync(fd_out)

def decrypt_file(key, filename):
    # Obtenemos el verdadero nombre, que ahora será el de salida
    (output, _) = os.path.splitext(filename)

    with open(filename, 'rb') as fd_in:
        with open(output, 'wb') as fd_out:
            # Obtenemos el tamaño del archivo original
            filesize = struct.unpack('<Q', fd_in.read(struct.calcsize('<Q')))[0]

            while (True):
                # Leemos el tamaño de la división mas el tamaño del bloque,
                # pero se le multiplica por 2, porque se necesita calcular
                # el nonce, tag y el mismísimo texto.
                chunk = fd_in.read(chunk_size + AES.block_size * 2)

                if (len(chunk) == 0):
                    break

                nonce = chunk[:AES.block_size]
                tag = chunk[AES.block_size:AES.block_size * 2]
                ciphertext = chunk[AES.block_size * 2:]
                cipher = AES.new(key, AES.MODE_EAX, nonce)

                text_plain = cipher.decrypt(ciphertext)
                
                fd_out.write(text_plain)
                fd_out.truncate(filesize)
                fd_out.flush()
                os.fsync(fd_out)

Un buen resultado es expresado en una simple pero descriptiva imagen:


Encriptando una imagen del gato de salem nombrada como salem.jpg.enc


Imagen del gato de salem desencriptada y luego mostrada


La diferencia entre la primera y la segunda imagen es que el la primera se cifra la imagen original, mientras que en la segunda se elimina y se descifra para luego mostrarla.

Lecturas recomendadas

Esta es la última parte de esta serie de artículos meramente educativos y entretenidos. No hay necesidad de estar cabizbajo por ello, como les vengo mencionando deben leer mucho más allá de lo que se escribe aquí porque estos mundos son muy amplios, por eso recomiendo leer lo siguiente y mucho más:


  • https://docs.python.org/3/library/struct.html
  • https://docs.python.org/3/library/os.html
  • eli.thegreenplace.net/2010/06/25/aes-encryption-of-files-in-python-with-pycrypto
  • https://es.wikipedia.org/wiki/Generador_de_n%C3%BAmeros_pseudoaleatorios_criptogr%C3%A1ficamente_seguro
  • https://pycryptodome.readthedocs.io/en/latest/src/examples.html#generate-an-rsa-key
  • https://es.wikipedia.org/wiki/Entrop%C3%ADa_(computaci%C3%B3n)


~ DtxdF

No hay comentarios :
Write comentarios

Tu comentario es importante y nos motiva a seguir escribiendo...

Powered by Blogger .