En esta auxiliar veremos el modo de uso de la librería Cryptography, además de algunos ejercicios relacionados con ella.
En aplicaciones reales, es muy poco probable que tengas que usar estas primitivas de forma directa. Por lo mismo, la librería Cryptography las agrupa en un paquete denominado hazmat (en español, material peligroso que causa riesgos a la vida o al ambiente si no es manejado con precaución)
Puedes ver la documentación de Cryptography sobre este tema acá.
Criptography define la siguiente interfaz para el uso de Cifradores:
encryptor()
: Devuelve un contexto usable para encriptar datosdecryptor()
: Devuelve un contexto usable para desencriptar datosAl mismo tiempo, tanto encryptor
como decryptor
implementan los siguientes métodos
update(_msg_)
: Agrega bytes a encriptar. Devuelve parte de los bytes encriptados (puede quedarse con algunos que no completan un bloque todavía)finalize()
: Devuelve los bloques restantes a encriptar, paddeados de ser necesario para completar un bloque.Para levantar un cifrador de bloque, es necesario llamar al constructor de la clase Cipher
con un algoritmo y un modo:
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend() # Configuración que la librería pide pero no usa por un problema de diseño
key = os.urandom(32) # Llave usada por el cifrador de bloque
iv = os.urandom(16) # Vector de inicialización usado por el modo
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor() # Contexto de cifrado
ct = encryptor.update(b"mensaje secreto") # Entrega parte de lo encriptado
ct += encryptor.finalize() # Devuelve todo lo encriptado, en caso de haber quedado datos sin devolver anteriormente
decryptor = cipher.decryptor() # Contexto de descifrado
print(decryptor.update(ct) + decryptor.finalize()) # devuelve el texto completo descifrado
# La última instrucción devolvería b'mensaje secreto'
(Ejemplo obtenido de la documentación de Cryptography)
Los siguientes algoritmos son soportados por la librería:
En todos los casos, el argumento del constructor recibe la llave simétrica a usar para cifrar la información.
La librería soporta los siguientes modos:
En todos los casos se recibe como primer argumento un nonce o initialization vector. Este valor entrega aleatoriedad al resultado del modo usado, y puede ser público sin comprometer la seguridad del sistema.
Criptography define la siguiente interfaz para el uso de MAC.
update(msg)
: Similar al caso de cifrado, agrega bytes a firmar.finalize()
: Devuelve la firma producida sobre los datos recibidos por update
.verify(sig)
: Compara la firma producida sobre los datos recibidos por update
, con la firma recibida.La librería soporta los siguientes tipos de MAC:
default_backend()
)default_backend()
)La documentación de Cryptography da el siguiente ejemplo para el MAC Poly1305:
from cryptography.hazmat.primitives import poly1305
p = poly1305.Poly1305(key)
p.update(b"message to authenticate")
print(p.finalize())
b'T\xae\xff3\xbdW\xef\xd5r\x01\xe2n=\xb7\xd2h'
p = poly1305.Poly1305(key)
p.update(b"message to authenticate")
p.verify(b"an incorrect tag") # Debería tirar una excepción
Cryptography además provee de los siguientes modos de cifrado que integran autentificación, combinando tipos generalmente usados en conjunto:
Ejemplo con ChaCha20Poly1305 de documentación de Cryptography:
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
data = b"a secret message"
aad = b"authenticated but unencrypted data"
key = AESGCM.generate_key(bit_length=128)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ct = aesgcm.encrypt(nonce, data, aad)
print(aesgcm.decrypt(nonce, ct, aad))
# Devuelve b'a secret message'
La interfaz de las funciones de hash en Cryptography define los siguientes métodos:
update(msg)
: Similar al caso de cifrado, agrega bytes a hashear.finalize()
: Devuelve el hash producido sobre los datos recibidos por update
.La librería soporta las siguientes funciones de Hash:
Ejemplo de la librería:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(b"abc")
digest.update(b"123")
print(digest.finalize())
# mostraría b'l\xa1=R\xcap\xc8\x83\xe0\xf0\xbb\x10\x1eBZ\x89\xe8bM\xe5\x1d\xb2\xd29%\x93\xafj\x84\x11\x80\x90'
En Python, se suele usar la función os.urandom(len)
para generar aleatoriedad criptográficamente segura. Sin embargo, como se vio en clases, muchas veces esta seguridad depende de la cantidad de entropía a la que nuestro computador tiene acceso.
En Linux, la función anteriormente mencionada obtiene aleatoriedad del dispositivo /dev/urandom
, mientras que en Windows se obtiene de la función CryptGenRandom
.
Por lo tanto, al crear variables aleatorias para su uso en otras funciones criptográficas (como por ejemplo, para claves o vectores de inicialización), se debe usar os.urandom(len)
.
Desde Python 3.6, es posible usar la función secrets.token_bytes(n)
de la librería secrets para obtener un arreglo de bytes de largo n.
A continuación veremos algunos problemas vistos en clase, pero usando código en Python. El código fuente usado se puede encontrar en la sección Material Docente del curso.
En clases vimos que el Modo ECB puede filtrar información estructural de los datos encriptados. El típico ejemplo de este problema es la siguiente imagen de la mascota de Linux, Tux.
(La historia de cómo esta imagen se hizo tan famosa es bastante interesante, y la pueden encontrar acá)
El código encrypt_image.py
toma la imagen tux.png
y la encripta en modo ECB, dejándola en el archivo tux_encrypted.png
. Si bien cada vez que ejecutes el código la imagen será distinta, la silueta se debiese de poder ver claramente en la mayoría de los casos.
A veces, al bajar archivos de Internet, es normal encontrarse con que al lado de lo descargado hay un hash. Este valor se usa para demostrar que el archivo que bajaste es el mismo que quién lo publicó quería que bajaras, ya que dentro de las propiedades de un buen hash criptográfico, se encuentra la dificultad de encontrar un segundo valor que al ser hasheado entregue el mismo hash que otro valor.
En algunos casos, hasta el día de hoy se siguen usando funciones de hash rotas (como MD5 y SHA1) para realizar este cálculo, lo que hace que esta verificación pierda valor.
(Acá puedes ver un ejemplo que utiliza la vulnerabilidad SHAttered, descubierta el 2017, para generar 2 PDF distintos con el mismo valor de hash)
Sin embargo, incluso usando funciones de hash seguras, nada asegura que el usuario comparará letra por letra el hash calculado sobre el archivo con el hash publicado. Muchas veces, uno se conforma comparando los primeros y/o últimos caracteres de ambos hashes.
Se les pide modificar script_virus.sh
, de tal forma que sus primeros 2 y últimos 2 caracteres del hash SHA256 sean los mismos que los del archivo script_bueno.sh
, y siga ejecutando el código que ejecuta actualmente.
Para determinar cómo leer y calcular el hash de un archivo, basarse en el script calculate_hash.py
adjunto en el material de la clase.
Usando la versión modificada del generador pseudoaleatorio definido en clases, disponible en el archivo prng.py
, generar una clave de 32 bytes. Luego, intentar adivinar la clave generada aprovechándose de un problema de implementación fundamental de este generador.
SEED_SIZE
?KEY_SIZE
?