Clase 04 · 44 minGratis

Encriptación simétrica con TDD

Clase 4 del curso gratis de buenas prácticas en Python: escribimos primero las pruebas y después la función, encriptamos con Fernet y aprendemos a probar funciones que no son deterministas.

Toca cualquier marca ▸ 0:00 de los apuntes y el video salta a ese momento. Abrir en YouTube

Simétrica y asimétrica

En la encriptación simétrica, la misma llave sirve para encriptar y para desencriptar. En la asimétrica hay dos: una pública para encriptar y una privada para desencriptar. Aquí implementamos la simétrica.

Pistas de tipo y docstrings

Desde Python 3.5 puedes anotar el tipo de cada argumento y del retorno. Python no las hace cumplir (es de tipado dinámico), pero documentan la función y el editor las muestra al usarla:

python
def encriptando_texto(mensaje: str, llave: str) -> str:
    ...

Sobre el idioma : el estándar de industria es escribir código y documentación en inglés, porque en un equipo internacional es el idioma común. En el curso usamos español con fines educativos.

Primero la prueba

TDD invierte el orden: dejas solo el esqueleto de la función y escribes la prueba. La prueba falla (en rojo) y tu trabajo es hacerla pasar.

Hay una trampa que conviene ver una vez : con un solo ejemplo, basta con que la función devuelva siempre ese resultado para que la prueba pase. Por eso una prueba con un solo caso no prueba casi nada .

Fernet y las funciones puras

Para encriptar usamos cryptography, una librería probada; el curso no trata de inventar un algoritmo.

bash
pip install cryptography
python
"""
Este modulo trabajará una encriptacion simetrica!
"""
from cryptography.fernet import Fernet


def encriptando_texto(mensaje: str, llave: str) -> str:
    """
    Esta funcion hace un encriptado simetrico
    Parametros:
        - mensaje (str): es el mensaje a ser encriptado
        - llave (str): es la llave de encriptación
    Retorna:
        - (str): Mensaje encriptado
    """
    entorno_cifrado = Fernet(llave)
    mensaje_encriptado = entorno_cifrado.encrypt(mensaje)
    return mensaje_encriptado

Fernet en realidad trabaja con bytes (b"hola"), no con str. Las pistas de tipo quedan como str hasta la clase 8, cuando la API obliga a notar la diferencia y se corrigen.

La llave no se genera dentro de la función. Una función pura devuelve siempre la misma salida para la misma entrada , y eso es lo que se puede probar con confianza. Una llave aleatoria adentro la volvería impredecible.

¿Cómo se prueba algo que cambia cada vez?

Sorpresa: las pruebas fallan aunque la función esté bien. Fernet agrega una marca de tiempo y un valor aleatorio, así que el mismo mensaje encriptado dos veces da dos resultados distintos. Comparar contra un texto fijo no sirve.

La salida es probar la propiedad que sí importa: lo que encripto, lo puedo recuperar .

python
"""
Aqui se prueba la funcion de encriptado simetrico
"""
import pytest
from cryptography.fernet import Fernet
from app.funciones.encriptado import encriptando_texto

LLAVE = b"UyJ4iWnYDPOcZ3sCmwNZtKbIudZRYl5n7owVTiQ4r3o="


@pytest.mark.parametrize(
    "mensaje,llave",
    [
        (b"a", LLAVE),
        (b"b", LLAVE),
        (b"Este es mi curso de python", LLAVE),
    ],
)
def test_encriptando_texto(mensaje, llave):
    """Encripta con nuestra función y desencripta con Fernet."""
    salida = encriptando_texto(mensaje, llave)
    assert Fernet(llave).decrypt(salida) == mensaje

La función inversa

python
def desencriptando_texto(mensaje_encriptado: str, llave: str) -> str:
    """funcion para desencriptar mensaje con una llave"""
    entorno_cifrado = Fernet(llave)
    return entorno_cifrado.decrypt(mensaje_encriptado)

Su prueba es el espejo de la anterior: encripta con Fernet, desencripta con nuestra función y compara. Aquí sí se puede generar la llave al azar en cada caso, porque lo que se compara es el mensaje original. Y una regla de estilo que Pylint recuerda : los import van arriba del archivo, no dentro de las funciones. (En el proyecto de esta clase todavía quedan dentro de las funciones de encriptado.py; se suben al pagar la deuda técnica de la clase 5.)

El reto de esta clase

Escribe tests/test_desencriptando_texto.py con al menos cinco mensajes: mayúsculas y minúsculas mezcladas, una arroba, números y un mensaje largo.

Ver una solución posible
python
"""
Aqui se prueba el modulo de desencriptar un texto
"""
import pytest
from cryptography.fernet import Fernet
from app.funciones.encriptado import desencriptando_texto


@pytest.mark.parametrize(
    "mensaje",
    [b"a", b"A", b"mensaje", b"MeNsAjE", b"@", b"este es un mens@je", b"m3ns@j3 d3 pru3b4!!"],
)
def test_desencriptando_texto(mensaje):
    """Encripta con Fernet y desencripta con la función a probar."""
    llave = Fernet.generate_key()
    mensaje_encriptado = Fernet(llave).encrypt(mensaje)
    assert desencriptando_texto(mensaje_encriptado, llave) == mensaje

Sigue con la clase 5

Git, deuda técnica y GitHub Actions

Seguir el canal de WhatsApp