Cifrar / descifrar datos en base de datos con Fernet (AES) y Fast API (Muy rápido y fácil)

Tiempo de lectura: 3 minutos

🚀 Descubre cómo asegurar tus datos con Fernet y FastAPI 🍹

Si eres un entusiasta de la programación web y te gusta disfrutar de un buen Fernet en tu tiempo libre, este artículo es para ti. En este post, exploraremos cómo puedes combinar dos elementos aparentemente dispares: Fernet, un sistema de cifrado de datos, y FastAPI, un marco web rápido y moderno para construir aplicaciones web API de alto rendimiento.

¿Qué es Fernet?

El Fernet, un destilado de hierbas con raíces en Italia, es conocido por su sabor único y su capacidad para unir a la gente en torno a una copa. En el mundo de la programación, Fernet toma un papel diferente: se convierte en una herramienta esencial para proteger tus datos. Fernet utiliza el cifrado AES para encriptar o desencriptar los datos dentro de una base de datos.

Vamos a ver una forma muy rápida de implementarlo en FAST API.

Primero tenemos que instalar la dependencia, bien en el fichero de requirements o bien como dependencia de pip:

pip install cryptography

Una vez instalada la dependencia, tendremos este archivo con los métodos más utilizados:

cryptografia.py:

from cryptography.fernet import Fernet
import os
from dotenv import load_dotenv 

load_dotenv()
FERNET_KEY = bytes(os.getenv('FERNET_KEY'), 'utf-8')

fernet = Fernet(FERNET_KEY)

def generate_key():
    key = Fernet.generate_key()
    return key.decode('utf-8')

#print (generate_key())

def encrypt_data(data):
    return fernet.encrypt(str(data).encode('utf-8')).decode('utf-8')


def decrypt_data(data):
    try:
        datosDecrypt = fernet.decrypt(data.encode()).decode('utf-8')
    except:
        datosDecrypt = data
    return datosDecrypt

Primero intentamos cargar la clave de fernet, esta clave será importantísima ya que nos servirá para cifrar y descifrar los datos. Sin ella, perdemos los datos. Por ello almacénala muy bien en tu llavero.

Con esta función obtenemos una clave de Fernet Aleatoria:

def generate_key():
    key = Fernet.generate_key()
    return key.decode('utf-8')

#print (generate_key())

En la primera ejecución, debemos ejecutar esa función (descomentando el print) y comentando la línea de : fernet = Fernet(FERNET_KEY)

Esto nos devolverá una clave aleatoria, podemos ejecutarlo tantas veces como queramos, hasta tener una clave que nos guste.

Una vez tengamos la clave, tenemos que crear un .env (en la raíz del proyecto y añadir la clave que hemos generado).

FERNET_KEY="Yt6dus7fasdgertmpoefv9i94059032ikjo-fmkfgmlkpe"

*Esta clave es ficticia, tendrás que añadir la tuya.

Ahora voy a explicar los otros dos métodos:

def encrypt_data(data):
    return fernet.encrypt(str(data).encode('utf-8')).decode('utf-8')

Este método (encrypt_data) cifra los datos, podemos usarlo antes de almacenar los datos en la base de datos (BBDD).

def decrypt_data(data):
    try:
        datosDecrypt = fernet.decrypt(data.encode()).decode('utf-8')
    except:
        datosDecrypt = data
    return datosDecrypt

El método decrypt_data descifra los datos, esto se usará para los get de la base de datos. En este método he incluido un try catch, ya que cuando obtiene un dato que no esté cifrado, saltaría error, en ese caso lo devuelve en crudo (esto es por si vuestros datos no estaban previamente cifrados).

Tenemos que tener cuidado con qué datos vamos a cifrar, ya que una vez cifrados no podremos filtrar por ellos (no de forma eficiente). Entonces intentaremos cifrar aquellos datos esenciales y que no tengamos que realizar búsquedas sobre ellos en el futuro (en la interfaz de APP, siempre podríamos descifrar la base de datos entera y realizar las búsquedas en crudo).

Y ya tenemos todo, ahora viene la magia.

Para implementar de forma rápida estos métodos, lo implementaremos directamente en los objetos de schema de nuestros datos antes de entrar en la BBDD.

Por ejemplo:

from util.cryptografia import decrypt_data, encrypt_data

class PostUsuario (BaseModel):
    id: int
    dni: str
    datos: str

    class Config:
        orm_mode = True,
        schema_extra = {
            "example": {
                "id": "1",
                "dni": "12345678A",
                "datos": "Mis datos secretos"
            }
        }
    
    def __init__(self, **data):
        # cifra los datos antes de crear el objeto
        data['dni'] = encrypt_data(data['dni'])
        data['datos'] = encrypt_data(data['datos'])

        super().__init__(**data)
 

Tal como muestro en el ejemplo importo dentro de la carpeta /util/cryptografia.py los métodos decrypt_data, encrypt_data y luego hago la magia.

Solo con incluir en el constructor de inicio __init__ del objeto Python en cuestión, la función encrypt_data aplicada al dato que queremos cifrar, se encargará de cifrar automáticamente los datos.

Esto debemos aplicarlo a nuestro método POST/UPDATE (previo a introducir datos en la base de datos).

Si tenemos un GET para obtener datos, tendremos que construir el objeto de la siguiente forma.

from util.cryptografia import decrypt_data, encrypt_data

class GetUsuario (BaseModel):
    id: int
    dni: str
    datos: str
  
    class Config:
        orm_mode = True,
        schema_extra = {
            "example": {
                "id": "1",
                "dni": "12345678A",
                "datos": "Mis datos secretos",
            }
        }
        
    def __init__(self, **data):
        # cifra los datos antes de crear el objeto
        data['dni'] = decrypt_data(data['dni'])
        data['datos'] = decrypt_data(data['datos'])
        

        super().__init__(**data)
    

En este ejemplo como antes, importo dentro de la carpeta /util/cryptografia.py los métodos decrypt_data, encrypt_data.

Y después añado un __init__ que se encarga de descifrar los datos que están cifrados previamente.

De esta manera no tendremos que modificar nuestras llamadas a la base de datos.

*Para que funcione correctamente, al devolver datos necesitamos devolver un objeto GetUsuario (construido a partir del schema que hemos modificado)

return GetUsuario(datosDevueltosCRUD.id, datosDevueltosCRUD.dni, datosDevueltosCRUD.datos)

Espero que te sirva.

Deja un comentario