93 lines
3.4 KiB
Python
93 lines
3.4 KiB
Python
"""
|
|
sobres.py — Las dos piezas de cifrado que comparten la entrada con contraseña,
|
|
la copia de seguridad y el cifrado de los datos.
|
|
|
|
· Una LLAVE es un número al azar de 32 bytes: es la que de verdad cifra.
|
|
· Un SOBRE guarda esa llave cerrada con un secreto que sabe una persona (su
|
|
contraseña, o su clave de recuperación). El secreto pasa por scrypt, que
|
|
es lento a propósito, para que probar contraseñas una a una no salga a
|
|
cuenta; con lo que sale se cierra la llave con AES-256-GCM.
|
|
|
|
Así la contraseña se puede cambiar sin volver a cifrar nada (solo se hace un
|
|
sobre nuevo para la misma llave), y la misma llave puede tener varios sobres:
|
|
uno con la contraseña y otro con la clave de recuperación.
|
|
|
|
Todo lo cifrado va con AES-256-GCM, que además detecta cualquier cambio.
|
|
Necesita el paquete «cryptography».
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import secrets
|
|
|
|
N, R, P = 2 ** 15, 8, 1 # scrypt: ~33 MB y unas décimas de segundo por intento
|
|
|
|
|
|
class ErrorCifrado(Exception):
|
|
"""Lo que se le dice al usuario tal cual."""
|
|
|
|
|
|
def _aesgcm(llave: bytes):
|
|
try:
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
except ImportError:
|
|
raise ErrorCifrado("Para cifrar hace falta el paquete «cryptography»: pip install cryptography") from None
|
|
return AESGCM(llave)
|
|
|
|
|
|
def _b64(b: bytes) -> str:
|
|
return base64.b64encode(b).decode()
|
|
|
|
|
|
def llave_nueva() -> bytes:
|
|
return secrets.token_bytes(32)
|
|
|
|
|
|
def cifrar(llave: bytes, contenido: bytes, contexto: bytes = b"") -> bytes:
|
|
"""nonce (12) + cifrado con su sello. «contexto» va sellado pero no cifrado."""
|
|
nonce = secrets.token_bytes(12)
|
|
return nonce + _aesgcm(llave).encrypt(nonce, contenido, contexto)
|
|
|
|
|
|
def descifrar(llave: bytes, cifrado: bytes, contexto: bytes = b"") -> bytes:
|
|
try:
|
|
return _aesgcm(llave).decrypt(cifrado[:12], cifrado[12:], contexto)
|
|
except ErrorCifrado:
|
|
raise
|
|
except Exception:
|
|
raise ErrorCifrado("No se puede descifrar: la llave no es la suya, o está dañado.") from None
|
|
|
|
|
|
def _de_secreto(secreto: str, sal: bytes, n: int, r: int, p: int) -> bytes:
|
|
return hashlib.scrypt(secreto.encode("utf-8"), salt=sal, n=n, r=r, p=p, dklen=32, maxmem=2 ** 27)
|
|
|
|
|
|
def envolver(llave: bytes, secreto: str, tipo: str = "clave") -> dict:
|
|
"""Un sobre: la llave cerrada con ese secreto."""
|
|
sal = secrets.token_bytes(16)
|
|
return {"tipo": tipo, "n": N, "r": R, "p": P, "sal": _b64(sal),
|
|
"cerrada": _b64(cifrar(_de_secreto(secreto, sal, N, R, P), llave, tipo.encode()))}
|
|
|
|
|
|
def abrir(sobre: dict, secreto: str) -> bytes | None:
|
|
"""La llave que guarda el sobre, o None si ese secreto no lo abre."""
|
|
try:
|
|
de_secreto = _de_secreto(secreto, base64.b64decode(sobre["sal"]), int(sobre["n"]), int(sobre["r"]), int(sobre["p"]))
|
|
return descifrar(de_secreto, base64.b64decode(sobre["cerrada"]), str(sobre["tipo"]).encode())
|
|
except ErrorCifrado as e:
|
|
if "cryptography" in str(e):
|
|
raise
|
|
return None
|
|
except (KeyError, ValueError, TypeError):
|
|
return None
|
|
|
|
|
|
def abrir_alguno(sobres: list[dict], secreto: str) -> bytes | None:
|
|
"""Prueba el secreto con cada sobre (el de la contraseña, el de la recuperación…)."""
|
|
for sobre in sobres or []:
|
|
llave = abrir(sobre, secreto)
|
|
if llave is not None:
|
|
return llave
|
|
return None
|