Crear un sistema de login en Python con archivos TXT es un proyecto educativo útil para practicar lectura y escritura de archivos, validación, funciones, manejo de errores y organización de datos. Sin embargo, debe entenderse como un ejercicio local. Un archivo de texto no es una solución adecuada para almacenar credenciales reales en producción, especialmente si guarda contraseñas sin protección.
Qué aprenderás con este proyecto
El objetivo es registrar usuarios, comprobar credenciales y evitar errores comunes. También aprenderás por qué una contraseña nunca debe guardarse como texto visible. Para reforzar la estructura del programa, consulta la guía de funciones en Python.
Estructura del archivo
Podemos utilizar un formato simple con un usuario y un hash separados por dos puntos:
ana:7b3d...
luis:98ac...Cada línea representa una cuenta. El sistema leerá el archivo, separará los campos y comparará un hash calculado. Este formato sigue siendo limitado, pero es mejor que guardar la contraseña original.
Por qué no guardar contraseñas en texto plano
Si el archivo contiene ana:mi_clave_123, cualquier persona con acceso puede leer la contraseña. Además, muchos usuarios reutilizan claves en otros servicios. Una filtración local puede afectar correos, redes sociales y cuentas bancarias.
Python incluye hashlib para calcular hashes. La documentación oficial de hashlib explica los algoritmos disponibles. Para contraseñas reales se recomiendan funciones especializadas y lentas, como Argon2, bcrypt, scrypt o PBKDF2, con salt y parámetros adecuados.
Crear una función de hash educativo
import hashlib
def crear_hash(contrasena: str) -> str:
return hashlib.sha256(
contrasena.encode("utf-8")
).hexdigest()SHA-256 directo sirve para demostrar el concepto, pero no es suficiente para un sistema real porque es demasiado rápido frente a ataques de fuerza bruta. Más adelante veremos una alternativa con PBKDF2.
Leer usuarios desde el archivo
from pathlib import Path
RUTA_USUARIOS = Path("usuarios.txt")
def cargar_usuarios() -> dict[str, str]:
usuarios = {}
if not RUTA_USUARIOS.exists():
return usuarios
with RUTA_USUARIOS.open(encoding="utf-8") as archivo:
for numero_linea, linea in enumerate(archivo, start=1):
linea = linea.strip()
if not linea:
continue
try:
usuario, hash_guardado = linea.split(":", maxsplit=1)
except ValueError:
print(f"Línea inválida: {numero_linea}")
continue
usuarios[usuario] = hash_guardado
return usuariosLa función devuelve un diccionario para búsquedas rápidas. La guía de type hints en Python explica las anotaciones utilizadas.
Registrar un usuario
def registrar_usuario(usuario: str, contrasena: str) -> bool:
usuario = usuario.strip().lower()
usuarios = cargar_usuarios()
if not usuario:
print("El usuario no puede estar vacío")
return False
if usuario in usuarios:
print("El usuario ya existe")
return False
if len(contrasena) < 8:
print("La contraseña debe tener al menos 8 caracteres")
return False
hash_guardado = crear_hash(contrasena)
with RUTA_USUARIOS.open("a", encoding="utf-8") as archivo:
archivo.write(f"{usuario}:{hash_guardado}\n")
print("Usuario registrado")
return TrueSe normaliza el nombre, se comprueba duplicidad y se valida una longitud mínima. Esta validación es básica; una política real debe adaptarse al contexto y evitar reglas arbitrarias que perjudiquen contraseñas largas.
Ocultar la contraseña en el terminal
El módulo getpass permite leer una contraseña sin mostrarla:
from getpass import getpass
usuario = input("Usuario: ")
contrasena = getpass("Contraseña: ")Consulta la guía de lectura segura de contraseñas en el terminal para conocer limitaciones y manejo de errores.
Validar el login
import hmac
def iniciar_sesion(usuario: str, contrasena: str) -> bool:
usuarios = cargar_usuarios()
usuario = usuario.strip().lower()
hash_guardado = usuarios.get(usuario)
if hash_guardado is None:
return False
hash_recibido = crear_hash(contrasena)
return hmac.compare_digest(hash_guardado, hash_recibido)compare_digest realiza una comparación diseñada para reducir diferencias de tiempo observables. En una aplicación educativa local es una buena práctica, aunque no convierte el sistema en una solución de producción.
Menú completo
def menu():
while True:
print("\n1. Registrar")
print("2. Iniciar sesión")
print("3. Salir")
opcion = input("Elige una opción: ").strip()
if opcion == "1":
usuario = input("Nuevo usuario: ")
contrasena = getpass("Nueva contraseña: ")
registrar_usuario(usuario, contrasena)
elif opcion == "2":
usuario = input("Usuario: ")
contrasena = getpass("Contraseña: ")
if iniciar_sesion(usuario, contrasena):
print("Acceso concedido")
else:
print("Credenciales incorrectas")
elif opcion == "3":
break
else:
print("Opción inválida")El mensaje de error no revela si falló el usuario o la contraseña. Esta práctica evita facilitar la enumeración de cuentas.
Usar salt con PBKDF2
Para mejorar el ejercicio, genera un salt aleatorio y utiliza PBKDF2:
import hashlib
import secrets
ITERACIONES = 600_000
def crear_registro(contrasena: str) -> tuple[str, str]:
salt = secrets.token_bytes(16)
derivado = hashlib.pbkdf2_hmac(
"sha256",
contrasena.encode("utf-8"),
salt,
ITERACIONES,
)
return salt.hex(), derivado.hex()La documentación oficial de secrets explica cómo generar valores criptográficamente seguros. Cada usuario debe tener un salt diferente.
Formato con salt
El archivo podría almacenar:
usuario:salt_hex:hash_hexAl iniciar sesión, se recupera el salt, se deriva nuevamente la clave con las mismas iteraciones y se compara el resultado.
Evitar inyección de líneas
No permitas caracteres como dos puntos o saltos de línea en el nombre de usuario:
def usuario_valido(usuario: str) -> bool:
return bool(usuario) and ":" not in usuario and "\n" not in usuarioTambién puede usarse una expresión regular para aceptar únicamente letras, números, guion y guion bajo.
Concurrencia y corrupción del archivo
Dos procesos que escriben al mismo tiempo pueden corromper el archivo o registrar duplicados. Un TXT no ofrece transacciones ni bloqueo robusto. Para aplicaciones con múltiples usuarios, utiliza una base de datos con restricciones únicas y operaciones atómicas.
Pruebas del sistema
Prueba registro correcto, usuario vacío, duplicados, contraseña corta, credenciales válidas, contraseña incorrecta, archivo inexistente y líneas dañadas. La guía de pruebas unitarias en Python ayuda a automatizar estos casos.
Variables y secretos
Si el proyecto evoluciona, no guardes configuraciones sensibles directamente en el código. Revisa cómo leer variables de entorno en Python.
Cuándo abandonar el archivo TXT
Utiliza una base de datos y un framework de autenticación cuando necesites recuperación de contraseña, sesiones, bloqueo de intentos, auditoría, permisos, múltiples procesos o exposición en Internet. No construyas autenticación crítica desde cero si existe una solución mantenida.
Conclusión
Un login con archivos TXT es un excelente ejercicio para practicar funciones, archivos, diccionarios, hashes y validación. Su valor está en el aprendizaje, no en la seguridad de producción. Mantén la advertencia visible, utiliza hashes con salt en la versión educativa y migra a componentes especializados cuando el proyecto deje de ser una demostración local.






