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.

Índice

    La entropía

    La entropía es la aleatoriedad que obtiene a través de un sistema operativo o una aplicación para su uso en los algoritmos o cálculos 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 cifradohí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 imagínense 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 cómodamente 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 cómoda 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 acómo usar RSAen 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
    Guardando el par de claves en el disco
    Leyendo las claves almacenadas en el disco con la aplicación cat
    Leyendo las claves almacenadas en el disco con la aplicación cat
     
    Importación realizada con éxito
    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'
    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
    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

    Encriptando una imagen del gato de salem nombrada como
    salem.jpg.enc
    Imagen del gato de salem desencriptada y luego mostrada
    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

    Deja una respuesta

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

    Subir

    Te has suscrito correctamente al boletín

    Se produjo un error al intentar enviar tu solicitud. Inténtalo de nuevo.

    Mi Diario Python will use the information you provide on this form to be in touch with you and to provide updates and marketing.