Un gestor de contraseñas almacena credenciales de forma organizada y evita reutilizar la misma contraseña en varios servicios. Construir uno con Python es un proyecto educativo útil para practicar cifrado, archivos, validación y menús de terminal. Sin embargo, un ejemplo casero no debe presentarse como sustituto automático de un gestor profesional auditado. La seguridad depende de muchos detalles que van más allá de ocultar texto.
En este proyecto utilizaremos Fernet, incluido en la biblioteca cryptography, para cifrado autenticado. También aplicaremos el módulo secrets de Python para generar contraseñas y pathlib para manejar archivos.
Qué debe proteger el programa
El archivo cifrado puede contener servicios, usuarios y contraseñas. La clave de cifrado debe mantenerse separada. Si un atacante obtiene tanto el archivo como la clave, podrá descifrarlo. Por eso un diseño real suele derivar la clave desde una contraseña maestra usando una función lenta como scrypt, Argon2 o PBKDF2, además de un salt aleatorio.
Para mantener el ejemplo comprensible, primero crearemos una versión con una clave almacenada en un archivo separado y después explicaremos cómo mejorarla.
Preparar el entorno
python -m venv .venv
# Windows PowerShell
.venv\Scripts\Activate.ps1
# macOS o Linux
source .venv/bin/activate
python -m pip install cryptographyNo instales la dependencia globalmente si puedes evitarlo. Guarda las versiones en un archivo de proyecto para reproducir el entorno.
Estructura del proyecto
gestor/
├── gestor.py
├── datos.enc
├── clave.key
└── .gitignoreAñade clave.key, datos.enc y cualquier archivo .env al .gitignore. El repositorio no debe contener secretos reales.
Generar una clave Fernet
from pathlib import Path
from cryptography.fernet import Fernet
RUTA_CLAVE = Path("clave.key")
def crear_clave() -> None:
if RUTA_CLAVE.exists():
raise FileExistsError("La clave ya existe")
RUTA_CLAVE.write_bytes(Fernet.generate_key())
crear_clave()Ejecuta esta operación una sola vez. Sobrescribir la clave impediría recuperar los datos cifrados anteriormente.
Cargar el cifrador
def cargar_fernet() -> Fernet:
if not RUTA_CLAVE.exists():
raise FileNotFoundError("No se encontró la clave")
return Fernet(RUTA_CLAVE.read_bytes())Comprueba los errores de forma explícita. Un programa no debe crear silenciosamente una clave nueva cuando falta la original, porque parecería que la base de datos está dañada.
Modelo de datos
Podemos guardar registros como JSON antes de cifrarlos.
import json
registros = [
{
"servicio": "correo",
"usuario": "[email protected]",
"contrasena": "ejemplo-no-real"
}
]
texto = json.dumps(registros, ensure_ascii=False).encode("utf-8")JSON facilita lectura y escritura, pero no aporta seguridad por sí mismo. Todo el contenido debe cifrarse antes de guardarlo.
Cifrar y guardar
RUTA_DATOS = Path("datos.enc")
def guardar(registros: list[dict[str, str]]) -> None:
fernet = cargar_fernet()
contenido = json.dumps(registros, ensure_ascii=False).encode("utf-8")
cifrado = fernet.encrypt(contenido)
RUTA_DATOS.write_bytes(cifrado)Fernet incluye autenticación. Si alguien modifica el archivo, el descifrado fallará en lugar de devolver datos alterados sin advertencia.
Descifrar y cargar
from cryptography.fernet import InvalidToken
def cargar() -> list[dict[str, str]]:
if not RUTA_DATOS.exists():
return []
fernet = cargar_fernet()
try:
contenido = fernet.decrypt(RUTA_DATOS.read_bytes())
except InvalidToken as error:
raise RuntimeError("Clave incorrecta o archivo dañado") from error
datos = json.loads(contenido.decode("utf-8"))
if not isinstance(datos, list):
raise ValueError("Formato de datos inválido")
return datosNo muestres detalles internos innecesarios al usuario. Registra el error de forma segura sin incluir contraseñas ni contenido descifrado.
Generar contraseñas seguras
import secrets
import string
def generar_contrasena(longitud: int = 20) -> str:
if longitud < 16:
raise ValueError("La longitud mínima es 16")
simbolos = "!@#$%&*+-_"
alfabeto = string.ascii_letters + string.digits + simbolos
while True:
valor = "".join(secrets.choice(alfabeto) for _ in range(longitud))
if (
any(c.islower() for c in valor)
and any(c.isupper() for c in valor)
and any(c.isdigit() for c in valor)
and any(c in simbolos for c in valor)
):
return valorTambién puedes estudiar el proyecto completo de generador seguro de contraseñas. La longitud aporta más seguridad que reglas excesivamente complicadas.
Añadir un registro
def agregar_registro(
registros: list[dict[str, str]],
servicio: str,
usuario: str,
contrasena: str,
) -> None:
servicio = servicio.strip()
usuario = usuario.strip()
if not servicio or not usuario or not contrasena:
raise ValueError("Todos los campos son obligatorios")
registros.append({
"servicio": servicio,
"usuario": usuario,
"contrasena": contrasena,
})Evita permitir servicios vacíos y decide cómo gestionar duplicados. Puede ser mejor actualizar un registro existente que crear varias entradas indistinguibles.
Buscar registros
def buscar(registros, termino):
termino = termino.strip().lower()
return [
registro
for registro in registros
if termino in registro["servicio"].lower()
or termino in registro["usuario"].lower()
]No imprimas todas las contraseñas de manera predeterminada. Muestra servicio y usuario, y revela la contraseña únicamente después de una acción explícita.
Leer la contraseña maestra sin mostrarla
El módulo getpass evita que la entrada aparezca en el terminal:
from getpass import getpass
maestra = getpass("Contraseña maestra: ")La guía para leer contraseñas de forma segura en el terminal explica limitaciones y validación. Recuerda que ocultar la entrada no protege contra malware, keyloggers o un sistema comprometido.
Derivar la clave desde una contraseña maestra
Guardar clave.key junto al archivo cifrado es cómodo, pero ofrece poca protección si ambos se copian. Una mejora consiste en derivar la clave mediante scrypt y un salt aleatorio.
import base64
import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
def derivar_clave(contrasena: str, salt: bytes) -> bytes:
kdf = Scrypt(
salt=salt,
length=32,
n=2**14,
r=8,
p=1,
)
clave = kdf.derive(contrasena.encode("utf-8"))
return base64.urlsafe_b64encode(clave)
salt = os.urandom(16)El salt no necesita ser secreto y puede almacenarse junto al archivo. Los parámetros deben equilibrar seguridad y rendimiento. Un sistema real también necesita migración de parámetros y manejo cuidadoso de la memoria.
No confundir cifrado con hash
Las contraseñas de usuarios de una aplicación normalmente se almacenan mediante hash lento, no cifrado reversible. Un gestor necesita recuperar la contraseña original, por eso utiliza cifrado. Para autenticación, consulta la guía de hash de contraseñas con bcrypt.
Crear un menú de terminal
def mostrar_menu():
print("1. Añadir")
print("2. Buscar")
print("3. Generar contraseña")
print("4. Salir")
while True:
mostrar_menu()
opcion = input("Opción: ").strip()
if opcion == "1":
pass
elif opcion == "2":
pass
elif opcion == "3":
print(generar_contrasena())
elif opcion == "4":
break
else:
print("Opción inválida")La guía de menús interactivos en Python presenta una estructura más modular con funciones y diccionarios.
Copias de seguridad
Una copia debe incluir el archivo cifrado y el material necesario para derivar o recuperar la clave. Guarda ambos con controles de acceso adecuados, pero evita colocarlos juntos en un lugar público. Prueba la restauración; una copia que nunca se ha restaurado no está verificada.
Bloqueo y concurrencia
Si dos procesos escriben el archivo al mismo tiempo, pueden sobrescribir cambios. Un proyecto más completo necesita bloqueo de archivo, escritura atómica en un archivo temporal y reemplazo final. También debería conservar una copia anterior en caso de interrupción.
Portapapeles
Copiar contraseñas al portapapeles mejora la comodidad, pero crea otro riesgo. Limpia el portapapeles después de un tiempo razonable y advierte al usuario. No registres ni muestres el valor durante la operación.
Limitaciones del proyecto
Este ejemplo no incluye auditoría externa, sincronización segura, protección de memoria, autofill, detección de phishing, hardware seguro ni recuperación avanzada. Por eso es adecuado para aprender, no para almacenar inmediatamente todas las credenciales críticas de una empresa.
Errores frecuentes
Los problemas más graves son guardar la clave junto al código, subir archivos al repositorio, inventar un algoritmo criptográfico, reutilizar una contraseña maestra débil, registrar secretos y sobrescribir la clave. También es peligroso pensar que codificación Base64 equivale a cifrado.
Fuentes confiables
La documentación oficial de Fernet en cryptography explica cifrado autenticado, claves y rotación. La guía de gestión de secretos de OWASP cubre almacenamiento, acceso, rotación y auditoría.
Conclusión
Un gestor sencillo con Python puede cifrar registros, generar contraseñas y practicar un diseño más seguro. Mantén la clave separada, usa primitivas auditadas, valida datos y evita exponer secretos. Para uso crítico, prefiere productos profesionales revisados y utiliza este proyecto como laboratorio para comprender los componentes que hacen posible su seguridad.






