La lliçó anterior va acabar amb una pregunta trampa: si l'empremta SHA-256 viatja junt amb el missatge, què impedeix que un atacant en substitueixi les dues coses? Res — i aquesta és exactament la diferència entre integritat (els bytes no s'han corromput) i autenticitat (els bytes vénen de qui diu que els envia). Aquesta lliçó presenta l'eina que dona totes dues coses alhora quan emissor i receptor comparteixen una clau: el MAC (Message Authentication Code), i la seva encarnació estàndard, HMAC. Pel camí entendràs per què la drecera intuïtiva hash(clau + missatge) està trencada (el length extension attack), i saldarem dos deutes concrets de MediNube: signar els webhooks que el portal envia a Clínica Sol i Centre Mèdic Luna, i deixar de desar en clar el token de recuperació de contrasenya que vam generar a 01-03. És la peça que converteix el hash de la lliçó anterior en una garantia davant d'enemics, no només davant de discos que corrompen bits.

Contingut

  1. La demostració: un hash a seques no autentica
  2. Què és un MAC i què garanteix (i què no)
  3. Per què hash(clau + missatge) està trencat: length extension
  4. HMAC: la construcció correcta
  5. HMAC en Python: el mòdul hmac
  6. Cas central: signar els webhooks de MediNube
  7. Deute saldat: el hash del token de recuperació
  8. El MAC que ja feies servir sense saber-ho: el tag de AES-GCM
  9. Hash vs MAC vs signatura digital

La demostració: un hash a seques no autentica

MediNube notifica per HTTP a les clíniques quan hi ha novetats ("nova cita creada", "informe disponible"). Primer intent heretat, trobat al codi del portal:

# codi heretat - notificador de webhooks
import hashlib, json

def enviar_webhook_MALAMENT(esdeveniment: dict) -> dict:
    cos = json.dumps(esdeveniment).encode("utf-8")
    empremta = hashlib.sha256(cos).hexdigest()
    return {"cos": cos, "capcalera_integritat": empremta}

El receptor (Clínica Sol) recalcula el SHA-256 del cos i el compara amb la capçalera. Sembla raonable... fins que ho mires com a atacant. Suposa que controles un proxy entre MediNube i la clínica:

import hashlib, json

# El missatge legitim interceptat:
paquet = enviar_webhook_MALAMENT({"evento": "cita_creada", "paciente": "ana.perez"})

# L'atacant fabrica UN ALTRE missatge i en recalcula el hash:
fals = json.dumps({"evento": "cita_cancelada", "paciente": "ana.perez"}).encode()
paquet["cos"] = fals
paquet["capcalera_integritat"] = hashlib.sha256(fals).hexdigest()

# La verificacio del receptor... passa sense problemes:
ok = hashlib.sha256(paquet["cos"]).hexdigest() == paquet["capcalera_integritat"]
print(ok)  # True - la clinica accepta el missatge fals

La verificació és matemàticament correcta i completament inútil: SHA-256 és una funció pública (Kerckhoffs, regla d'or 2) que no requereix res que l'atacant no tingui. El hash només lliga la capçalera al cos; no lliga res a MediNube. Per a això, en el càlcul hi ha d'intervenir alguna cosa que l'atacant no conegui: una clau secreta compartida.

Què és un MAC i què garanteix (i què no)

Un MAC és una funció que agafa una clau secreta i un missatge i produeix una etiqueta curta (el tag):

tag = MAC(clau, missatge)

El receptor, que comparteix la clau, recalcula el tag i el compara. La propietat de seguretat s'anomena resistència a la falsificació existencial: un atacant que vegi tants parells (missatge, tag) com vulgui no pot produir un tag vàlid per a cap missatge nou sense la clau. Amb això, un tag correcte demostra dues coses alhora:

  • Integritat: el missatge no ha canviat ni un bit des que es va calcular el tag (l'efecte allau del hash subjacent continua treballant per a nosaltres).
  • Autenticitat: l'ha generat algú que coneix la clau. Si només la coneixen MediNube i Clínica Sol, i la clínica no se l'ha enviat a si mateixa, ha vingut de MediNube.

Igual d'important és el que un MAC no dona:

  • No dona confidencialitat. El missatge viatja tal com estigui (en clar, en el nostre webhook); el MAC només l'autentica. Si a més cal ocultar-lo, es xifra (amb AEAD, que com veurem ja inclou el seu propi MAC).
  • No dona no repudi. La clau és compartida: qualsevol missatge que MediNube va poder autenticar, Clínica Sol també l'hauria pogut fabricar (i viceversa). Davant d'un jutge, "té un MAC vàlid" no prova quina de les dues parts el va generar. Quan calgui que només una part pugui generar la prova i qualsevol verificar-la — les receptes electròniques que tenim pendents — necessitarem signatures digitals amb clau asimètrica: lliçó 04-03.

Per què hash(clau + missatge) està trencat: length extension

Amb la idea de "barrejar una clau", el següent codi heretat gairebé sembla la solució:

def tag_MALAMENT(clau: bytes, missatge: bytes) -> str:
    return hashlib.sha256(clau + missatge).hexdigest()   # NO: length extension

Està trencat, i la raó ensenya molt sobre com funcionen els hashes per dins. Recorda la "picadora" Merkle–Damgård de 03-01: SHA-256 processa el missatge per blocs, actualitzant un estat intern, i el hash final és, literalment, l'estat intern després de l'últim bloc. No hi ha cap pas final que ho disfressi.

Conseqüència: si un atacant coneix sha256(clau + missatge), coneix l'estat intern de la picadora en aquell punt. Pot carregar aquest estat i continuar picant: calcular sha256(clau + missatge + farciment + extra) per a un extra de la seva elecció, sense conèixer la clau (només en necessita la longitud, que s'endevina provant). Això és el length extension attack:

flowchart LR
    A[estat inicial] -->|"clau + missatge"| B[estat S]
    B -->|"= tag publicat"| C[l'atacant LLEGEIX S del tag]
    C -->|"+ farciment + extra"| D[tag valid del missatge estes]

Traduït a MediNube: del webhook legítim {"evento": "cita_creada"...} amb el seu tag_MALAMENT, l'atacant deriva un tag vàlid per a aquest mateix cos amb contingut extra afegit — sense la clau. En APIs reals (paràmetres d'URL signats, per exemple) aquest atac s'ha explotat durant anys; el cas cèlebre va ser l'API de Flickr el 2009.

Apunts per completar el mapa:

  • Afecta els hashes Merkle–Damgård "purs": MD5, SHA-1, SHA-256, SHA-512. No afecta SHA-3 (l'esponja no exposa el seu estat complet), ni BLAKE2, ni SHA-384 (sortida truncada: falten bits de l'estat).
  • hash(missatge + clau) (la clau al final) evita aquest atac però hereta altres defectes (una col·lisió del hash es converteix en falsificació del MAC). Els pedaços casolans sempre acaben així — regla d'or 1: no inventis la teva pròpia criptografia, ni tan sols "combinacions" pròpies de peces bones.

HMAC: la construcció correcta

HMAC (1996, RFC 2104) és la forma estandarditzada i demostrada de construir un MAC a partir de qualsevol funció hash. El seu esquema:

HMAC(K, m) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || m ) )

No cal memoritzar-lo, però sí entendre la jugada:

  • La clau es barreja dues vegades, amb dues constants diferents (ipad, farciment intern; opad, extern), i hi ha dues passades de hash imbricades.
  • La passada externa és el que mata el length extension: l'estat intern del hash que ha processat el missatge ja no és el tag — el tag és el hash d'aquest resultat junt amb la clau una altra vegada. L'atacant no pot "continuar picant" perquè el resultat intermedi mai no es publica.
  • HMAC té prova de seguretat: si el hash subjacent compleix propietats raonables, HMAC és un MAC segur. Tan robust és el disseny que HMAC-MD5 continua sense atacs pràctics tot i que MD5 estigui trencat en col·lisions — la qual cosa no és una excusa per fer-lo servir, però dona idea del marge.
  • HMAC-SHA256 produeix tags de 32 bytes i és l'estàndard de facto en APIs. Ja l'has fet servir sense mirar-lo: és la peça que PBKDF2 itera milers de vegades i el motor intern d'HKDF (02-04).

HMAC en Python: el mòdul hmac

La biblioteca estàndard ho porta tot:

import hmac, hashlib

clau = bytes.fromhex(
    "8f4e2a11c96d3b70e5a8d4c2f01b9e6633d7a0558c1f4b2e9d60a3517c8e4f0b"
)  # 32 bytes del CSPRNG, compartits amb la clinica (ficticia, es clar)

missatge = b'{"evento": "cita_creada", "paciente": "ana.perez"}'

tag = hmac.new(clau, missatge, hashlib.sha256).hexdigest()
print(tag)  # p. ex. 3e19c19d...

Desglossament:

  • hmac.new(clau, missatge, digestmod) crea i alimenta l'HMAC. El tercer argument (la funció hash) és obligatori especificar-lo; fes servir hashlib.sha256. Com els objectes de hashlib, admet .update() per a missatges a trossos i .digest()/.hexdigest() per al tag.
  • La clau ha de sortir del CSPRNG (regla d'or 3): 32 bytes de secrets.token_bytes(32) en donar-la d'alta. Mai una contrasenya ni un valor "memorable".
  • Per verificar, mai ==:
def verificar(clau: bytes, missatge: bytes, tag_rebut: str) -> bool:
    esperat = hmac.new(clau, missatge, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperat, tag_rebut)

Aquí sí que estem comparant un secret: el tag correcte és una cosa que l'atacant no coneix i vol endevinar. Un == normal es rendeix al primer byte diferent, i aquesta diferència de temps, mesurada amb paciència, permet endevinar el tag byte a byte — el timing attack de 01-04. hmac.compare_digest (equivalent a secrets.compare_digest) triga el mateix coincideixi el que coincideixi: regla d'or 6 aplicada on toca. Contrasta amb l'empremta pública de 03-01, on == era legítim — el criteri és sempre "és secret el que comparo?".

Cas central: signar els webhooks de MediNube

Anem a arreglar de veritat el notificador. El disseny segueix el patró que fan servir Stripe, GitHub o Slack per als seus webhooks, adaptat a casa nostra:

  • Cada clínica receptora té una clau de webhook pròpia (32 bytes de secrets.token_bytes(32)), lliurada en configurar la integració. Clau per clínica: si la de Centre Mèdic Luna es filtra, la de Clínica Sol continua a salvo, i se'n pot rotar una sense tocar l'altra (criptoagilitat, regla 8).
  • MediNube envia dues capçaleres: X-MediNube-Timestamp (segons Unix de l'enviament) i X-MediNube-Firma (el tag HMAC, prefixat amb la versió de l'esquema: v1=<hex>).
  • L'HMAC no cobreix només el cos, sinó timestamp || "." || cos. El sentit de separar amb . i d'incloure el timestamp és defensar-se de la repetició: sense això, un atacant que capturi un webhook legítim ("informe disponible") podria reenviar-lo mil vegades dies després, amb una signatura perfectament vàlida. En signar el timestamp, el receptor pot rebutjar missatges vells.
# medinube/webhooks.py - costat emissor (MediNube)
import hmac, hashlib, time

def signar_webhook(clau_clinica: bytes, cos: bytes) -> dict:
    """Genera les capcaleres d'autenticacio d'un webhook sortint."""
    timestamp = str(int(time.time()))
    base = timestamp.encode("utf-8") + b"." + cos
    tag = hmac.new(clau_clinica, base, hashlib.sha256).hexdigest()
    return {
        "X-MediNube-Timestamp": timestamp,
        "X-MediNube-Firma": f"v1={tag}",
    }

I la verificació, que MediNube documenta per als desenvolupadors de les clíniques:

# costat receptor (p. ex. el sistema de Clinica Sol)
import hmac, hashlib, time

TOLERANCIA = 300  # segons: 5 minuts de marge de rellotge

def verificar_webhook(clau: bytes, cos: bytes,
                      timestamp: str, signatura: str) -> bool:
    # 1. Frescor: rebutjar missatges vells (anti-repeticio)
    if abs(time.time() - int(timestamp)) > TOLERANCIA:
        return False
    # 2. Versio de l'esquema
    if not signatura.startswith("v1="):
        return False
    # 3. Recalcular i comparar en temps constant
    base = timestamp.encode("utf-8") + b"." + cos
    esperat = hmac.new(clau, base, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperat, signatura[3:])

Detalls que marquen la diferència entre aquest codi i un de vulnerable:

  • El timestamp verificat és el signat. L'atacant no pot "refrescar" un missatge capturat canviant la capçalera de timestamp, perquè el tag deixaria de quadrar. Repetició bloquejada més enllà de la finestra de 5 minuts (finestra que existeix només per tolerar rellotges desincronitzats). Si la teva aplicació no tolera cap repetició ni dins de la finestra, afegeix un identificador únic d'esdeveniment i aplica deduplicació — capa d'aplicació, no de criptografia (regla 9).
  • Es verifica sobre el cos cru (els bytes rebuts), no sobre el JSON re-serialitzat: dos serialitzadors poden ordenar claus o espaiar de manera diferent, i la signatura no coincidiria encara que el missatge fos legítim. Signa bytes, verifica bytes.
  • El prefix v1= és la mateixa jugada que el byte de versió del format d'historials de 02-03: si demà migrem l'esquema de signatura, els dos costats poden conviure amb v1 i v2 durant la transició.
  • Nota de context: els webhooks viatgen a més per HTTPS (Mòdul 5), que ja xifra i autentica el canal. La signatura HMAC autentica el missatge d'extrem a extrem: sobreviu a proxies, cues, reintents i logs intermedis, i permet a la clínica validar el webhook encara que qui acabi el TLS sigui un balancejador aliè.

Deute del curs saldat: els webhooks sortints de MediNube van signats.

Deute saldat: el hash del token de recuperació

Segon deute, aquest ve de 01-03. Allà vam generar tokens de recuperació de contrasenya amb secrets.token_urlsafe(32) i els desàvem tal qual a la base de dades — amb la promesa d'arreglar-ho. El problema: si un atacant llegeix la BD (injecció SQL, còpia de seguretat filtrada), obté tokens vàlids i utilitzables per restablir la contrasenya de qualsevol usuari, inclosa ana.perez.

La solució és la mateixa idea que aplicarem a les contrasenyes a 03-03, però amb una diferència crucial que convé entendre ja: desar el hash del token, no el token.

# medinube/recuperacio.py
import hashlib, secrets

def crear_token_recuperacio(usuari: str) -> str:
    token = secrets.token_urlsafe(32)          # 32 bytes d'entropia = 256 bits
    index = hashlib.sha256(token.encode("utf-8")).hexdigest()
    desar_a_bd(usuari, index)                  # es desa el hash, no el token
    return token                                # el token nomes viatja al correu

def bescanviar_token(token_presentat: str):
    index = hashlib.sha256(token_presentat.encode("utf-8")).hexdigest()
    return cercar_a_bd(index)                   # cerca exacta pel hash
  • El token real només existeix al correu de l'usuari i en memòria durant la petició. La BD desa sha256(token): si es filtra, l'atacant té empremtes de les quals no pot recuperar els tokens (resistència a preimatge sobre una entrada de 256 bits d'entropia — aquí sí que és imprevisible de veritat).
  • Per què n'hi ha prou amb un SHA-256 ràpid, si a 03-01 dèiem que "hash ràpid" i "secret" no es porten bé? Per l'entropia de l'entrada. Una contrasenya humana s'ataca per diccionari: l'espai de candidats és petit i un hash ràpid el recorre volant. Aquest token té 256 bits d'entropia del CSPRNG: no hi ha diccionari possible, l'atac de força bruta és 2^256. La lentitud d'Argon2 és un pedaç per a la baixa entropia humana; amb entropia plena, sobra. Aquest contrast és exactament la porta d'entrada de 03-03.
  • Bonus de disseny: en ser el hash determinista, serveix com a índex de cerca a la BD (cerca exacta per index). Una variant igual de vàlida és desar HMAC(clau_servidor, token), que a més exigeix comprometre la clau del servidor per, com a mínim, poder verificar candidats; per a un token de 256 bits, SHA-256 a seques ja és suficient i més senzill.
  • La comparació es fa implícitament en cercar l'índex a la BD; si la teva implementació compara el hash "a mà", torna a ser un secret derivat: compare_digest. I recorda donar al token una caducitat curta i un sol ús — de nou, capa d'aplicació.

El MAC que ja feies servir sense saber-ho: el tag de AES-GCM

Si HMAC et sona conceptualment familiar és perquè el Mòdul 2 ja et va fer servir un MAC a diari: el tag d'autenticació d'AES-GCM (02-03) és exactament això — un MAC (GMAC, basat en aritmètica de polinomis en lloc de hashes) calculat sobre el xifrat i l'AAD, que és el que fa saltar la nostra excepció HistorialManipulat quan algú toca un historial xifrat. I Poly1305, la meitat de ChaCha20-Poly1305, és un altre MAC d'aquesta família d'"un sol ús per clau derivada", dissenyat per ser rapidíssim. La jerarquia queda així:

  • Xifres i autentiques alhora? → AEAD (el MAC hi va inclòs; no li afegeixis un HMAC a sobre).
  • Autentiques sense xifrar (el receptor necessita llegir el missatge, com els nostres webhooks)? → HMAC.

Hash vs MAC vs signatura digital

La taula que ordena aquest mòdul i anticipa el següent:

Hash (03-01) MAC/HMAC (aquesta lliçó) Signatura digital (04-03)
Necessita clau No Sí, compartida (simètrica) Sí, parell pública/privada
Integritat
Autenticitat No Sí (entre qui comparteix la clau) Sí (davant de qualsevol)
No repudi No No (totes dues parts poden generar el tag) (només el propietari de la privada signa)
Qui pot verificar Qualsevol Només qui té la clau Qualsevol, amb la clau pública
Cost Molt baix Molt baix Alt (mil vegades més lent)
A MediNube Empremta d'exports Webhooks, índex de tokens Receptes electròniques (pendent)

Errors Comuns i Consells

  • hash(clau + missatge) casolà. Length extension sobre SHA-256. Existeix hmac.new; fes-lo servir. Si algun dia veus sha256(secret + dades) en un codi heretat, és una troballa d'auditoria, no una peculiaritat d'estil.
  • Verificar el tag amb ==. Timing attack sobre un secret. hmac.compare_digest, sempre, als dos costats de la integració (regla d'or 6).
  • Signar el JSON re-serialitzat en lloc dels bytes rebuts. La signatura s'ha de calcular i verificar sobre els bytes crus del cos; re-serialitzar introdueix diferències invisibles (ordre de claus, espais, unicode) que trenquen signatures legítimes — i els "arranjaments" apressats d'això solen acabar debilitant la verificació.
  • Oblidar el timestamp (o no incloure'l en allò signat). Sense ell, qualsevol webhook capturat és reproduïble per sempre. I si el timestamp viatja però no és dins de l'HMAC, l'atacant l'actualitza i la repetició reviu.
  • Reutilitzar la mateixa clau de webhook per a totes les clíniques o derivar la clau d'una cosa endevinable. Clau per receptor, del CSPRNG, rotable. Si necessites diverses claus d'un secret mestre, ja coneixes l'eina: HKDF amb info diferent (02-04).
  • Afegir HMAC a sobre d'AES-GCM "per a més seguretat". El tag de GCM ja és un MAC sobre xifrat i AAD; duplicar-lo afegeix complexitat i superfície d'error, no seguretat.
  • Consell: quan dissenyis quins bytes signar, pregunta't què podria canviar un atacant sense invalidar el tag. Tot allò que hagi de ser immutable (timestamp, versió, destinatari) ha d'estar dins d'allò signat — la mateixa lògica que l'AAD de 02-03.

Exercicis

Exercici 1. Aquest endpoint heretat verifica webhooks entrants al sistema de Centre Mèdic Luna. Troba els tres errors i explica l'atac que permet cadascun:

def verificar_MALAMENT(clau, cos, capcaleres):
    tag = hashlib.sha256(clau + cos).hexdigest()
    return tag == capcaleres["X-MediNube-Firma"]

Exercici 2. MediNube vol afegir a cada webhook la clínica de destinació, de manera que un webhook signat per a Clínica Sol no es pugui presentar davant de Centre Mèdic Luna ni tan sols si totes dues compartissin clau per un error de configuració. Modifica signar_webhook/verificar_webhook per incloure l'identificador de la clínica en el material signat, sense afegir capçaleres noves que el receptor hagi de llegir (pista: el receptor ja sap qui és).

Exercici 3. Un company proposa desar els tokens de recuperació amb Argon2 "perquè és el més segur per a secrets, millor que SHA-256". Explica (a) per què no aporta seguretat real en aquest cas, i (b) quin cost operatiu introdueix que SHA-256 no té (pista: pensa en com es cerca el token a la BD, sabent que Argon2 fa servir sal aleatòria).

Solucions

Solució 1. (1) sha256(clau + cos) és la construcció vulnerable a length extension: un atacant pot estendre un webhook legítim amb contingut extra i calcular un tag vàlid sense la clau — ha de ser hmac.new(clau, cos, hashlib.sha256). (2) tag == ... compara un secret amb ==: timing attack que permet reconstruir el tag vàlid byte a byte — ha de ser hmac.compare_digest. (3) No hi ha timestamp ni comprovació de frescor: qualsevol webhook capturat es pot reproduir indefinidament — cal signar timestamp || "." || cos i rebutjar missatges fora de la finestra de tolerància.

Solució 2. S'inclou l'identificador en la base signada; cada receptor verifica amb el seu propi identificador, que ja coneix, sense capçalera nova:

def signar_webhook(clau: bytes, cos: bytes, clinica: str) -> dict:
    timestamp = str(int(time.time()))
    base = f"{timestamp}.{clinica}.".encode("utf-8") + cos
    tag = hmac.new(clau, base, hashlib.sha256).hexdigest()
    return {"X-MediNube-Timestamp": timestamp, "X-MediNube-Firma": f"v1={tag}"}

def verificar_webhook(clau: bytes, cos: bytes, timestamp: str,
                      signatura: str, el_meu_id: str = "clinica-sol") -> bool:
    if abs(time.time() - int(timestamp)) > 300 or not signatura.startswith("v1="):
        return False
    base = f"{timestamp}.{el_meu_id}.".encode("utf-8") + cos
    esperat = hmac.new(clau, base, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperat, signatura[3:])

Si el webhook anava adreçat a una altra clínica, la base reconstruïda difereix i el tag no quadra. És el mateix principi que l'AAD paciente=... de 02-03: lligar el context al material autenticat. (En un canvi real caldria pujar l'esquema a v2=, perquè els tags de v1 ja no serien compatibles.)

Solució 3. (a) Argon2 és lent a propòsit per compensar la baixa entropia de les contrasenyes humanes (espai de candidats petit → cal encarir cada intent). El token té 256 bits d'entropia del CSPRNG: l'espai de candidats és 2^256 i cap atacant el pot enumerar, a qualsevol velocitat. SHA-256 ja deixa la força bruta en "impossible"; Argon2 la deixaria en "impossible però gastant la teva RAM". (b) Argon2 genera una sal aleatòria per hash: el mateix token produeix hashes diferents cada vegada, així que no pots localitzar el registre calculant el hash del token presentat — hauries d'executar verify (lent i amb memòria alta) contra cada fila candidata de la taula. SHA-256, determinista, funciona com a índex de cerca exacta i instantània. Eina segons el problema: regla d'or 7.

Conclusió

El forat que va deixar 03-01 està tancat: quan empremta i missatge viatgen junts, la resposta és barrejar una clau secreta compartida en el càlcul — un MAC —, i la forma correcta de fer-ho amb hashes és HMAC, les dues passades imbricades del qual neutralitzen el length extension que trenca la drecera hash(clau + missatge). A MediNube això es va traduir en dos deutes saldats: els webhooks sortints viatgen amb X-MediNube-Firma: v1=<hmac> sobre timestamp.cos (anti-repetició inclosa, verificació amb compare_digest), i la BD ja no desa tokens de recuperació sinó el seu SHA-256 — hash ràpid que aquí sí que n'hi ha prou, perquè el token té 256 bits d'entropia i cap diccionari l'abasta.

Aquesta última frase és el trampolí de la propera lliçó: i quan el secret no té entropia de sobres? Les contrasenyes d'ana.perez i de la resta d'usuaris del portal són paraules humanes, previsibles, reutilitzades — i desar-les bé és el problema de seguretat més freqüent (i més freqüentment mal resolt) del desenvolupament web. Hem fregat la resposta dues vegades: les KDF lentes de 02-04 i l'"el hash ràpid no serveix" de 03-01. A 03-03: Emmagatzematge Segur de Contrasenyes ajuntem les peces: del desastre històric (text en clar, MD5 sense sal, rainbow tables) a Argon2id amb argon2-cffi, amb el flux complet de registre i inici de sessió de MediNube i la migració de hashes antics. És l'últim deute pendent d'aquest mòdul. Anem a saldar-lo.

© Copyright 2026. Tots els drets reservats