Arriba el deute més antic i més important del curs: com desa MediNube les contrasenyes amb què ana.perez i milers d'usuaris més inicien sessió a portal.medinube.example. L'hem fregat a 01-03 (tokens i entropia), l'hem esquivat a propòsit a 02-04 ("derivar claus per xifrar és un altre problema; el d'autenticar arribarà a 03-03") i la lliçó anterior ens va deixar a la porta: els hashes ràpids valen per a secrets d'alta entropia, i una contrasenya humana és justament el contrari. Aquesta lliçó recorre l'evolució històrica de l'error (text en clar → hash ràpid → hash amb sal → funcions deliberadament lentes), fixa la recomanació actual — Argon2id amb argon2-cffi — i l'aterra en el flux complet de registre i inici de sessió del portal, inclosa la migració transparent dels hashes heretats. Importa perquè és, de llarg, la fallada criptogràfica amb més víctimes reals: les filtracions de bases de dades d'usuaris es compten per milers de milions de credencials, i la diferència entre un incident molest i una catàstrofe és exactament el que aprendràs aquí.

Contingut

  1. L'escenari d'amenaça: et robaran la base de dades
  2. Verificador vs clau: en què es diferencia això de 02-04
  3. L'evolució de l'error, etapa per etapa
  4. Les funcions correctes: bcrypt, scrypt i Argon2id
  5. Argon2id en Python amb argon2-cffi
  6. El format modular: el hash que ho porta tot a dins
  7. Cas pràctic: registre i inici de sessió a MediNube, amb migració inclosa
  8. Què NO fer (encara que sembli bona idea)
  9. Més enllà de la contrasenya: MFA i passkeys

L'escenari d'amenaça: et robaran la base de dades

El disseny comença assumint el pitjor (regla d'or 2: l'enemic coneix el sistema — i aquí, a més, té el teu disc). L'escenari contra el qual es dissenya l'emmagatzematge de contrasenyes és concret: l'atacant obté una còpia completa de la taula d'usuaris. Vies sobradament reals: injecció SQL, una còpia de seguretat en un bucket mal configurat, el portàtil d'un desenvolupador, un empleat descontent.

Dos matisos que defineixen bé el problema:

  • No dissenyem per a l'inici de sessió en línia. Els intents contra el formulari de portal.medinube.example es frenen amb límits d'intents i bloquejos — capa d'aplicació (regla 9). Dissenyem per a l'atac offline: l'atacant, amb la taula a la seva màquina, prova candidats a la velocitat del seu maquinari, sense límits ni logs.
  • L'objectiu no és que cap contrasenya caigui (la de qui fa servir 123456 caurà sempre), sinó comprar temps i encarir l'atac massiu: que desxifrar el gruix de la taula costi anys de GPU en lloc d'una tarda, donant marge per detectar la bretxa, forçar reinicis de contrasenya i avisar els afectats. Amb dades sanitàries de per mig, a més, una bretxa real activa obligacions del RGPD (notificació a l'autoritat i als afectats) — recordatori de sempre: MediNube és fictici, i un sistema real d'aquest tipus passa per revisió de seguretat i compliance abans de producció.

Verificador vs clau: en què es diferencia això de 02-04

A 02-04 vam fer servir scrypt per convertir la contrasenya de l'administrador de Clínica Sol en una clau AES amb què xifrar una còpia de seguretat. Aquí fem servir la mateixa família de funcions... per no xifrar res. La distinció es mereix la seva taula, perquè confondre tots dos casos genera dissenys absurds:

Derivar clau per xifrar (02-04) Desar verificador per autenticar (avui)
Objectiu Obtenir 32 bytes utilitzables com a clau Poder comprovar "és aquesta la contrasenya?"
Es desa la sortida? No — la clau s'usa i es descarta — el verificador viu a la BD
La sortida obre alguna cosa? Sí: desxifra les dades No: per si sola no serveix per a res
Si un atacant la roba Desastre: desxifra la còpia de seguretat Ha de continuar atacant: només és un verificador
Necessita determinisme extern Sí: mateixos paràmetres per re-derivar Autocontingut: els paràmetres viatgen en el hash
Funció típica scrypt/Argon2 com a KDF bcrypt/scrypt/Argon2id com a password hasher

La idea nova és la de verificador: un valor que permet comprovar una contrasenya presentada però que no conté la contrasenya ni substitueix el fet de tenir-la. En l'inici de sessió, MediNube recalcula la funció sobre el que l'usuari ha teclejat i ho compara amb el que hi ha desat. La mateixa matemàtica lenta de 02-04, propòsit oposat.

L'evolució de l'error, etapa per etapa

Cada etapa d'aquesta història continua viva en codi heretat de producció. Convé conèixer-les totes, perquè te les trobaràs.

Etapa 0: text en clar

def desar_contrasenya_MALAMENT(usuari: str, contrasenya: str):
    bd.inserir(usuari, contrasenya)   # NO: la BD robada ES la llista de credencials

Robatori de BD = totes les credencials de tots els usuaris, a l'instant. I com que la gent reutilitza contrasenyes, el dany es propaga als seus comptes de correu i banca. Agreujant conegut: RockYou (2009, 32 milions de contrasenyes en clar) — el seu fitxer és avui el diccionari estàndard dels atacants. Si la teva aplicació et pot ensenyar la teva contrasenya actual o enviar-te-la per correu, és en aquesta etapa.

Etapa 1: hash ràpid sense sal

def desar_contrasenya_MALAMENT_v2(usuari: str, contrasenya: str):
    bd.inserir(usuari, hashlib.sha256(contrasenya.encode()).hexdigest())

Sembla resoldre-ho ("és unidireccional") i falla pels dos flancs que ja coneixes de 03-01:

  • Velocitat. SHA-256 està dissenyat per volar. Xifres orientatives del maquinari d'un aficionat amb unes quantes GPU modernes: de l'ordre de desenes de milers de milions de SHA-256 per segon (i ~10× més per a MD5). El diccionari RockYou sencer contra un hash: mil·lèsimes de segon. Totes les combinacions de 8 caràcters alfanumèrics: qüestió d'hores.
  • Determinisme global, sense sal. La mateixa contrasenya produeix el mateix hash a totes les files i a totes les empreses del món. Això habilita les rainbow tables — taules precalculades hash→contrasenya que es computen una vegada i es reutilitzen contra qualsevol BD — i de regal delata patrons: si tres usuaris comparteixen hash, comparteixen contrasenya.

Etapa 2: hash ràpid amb sal

Aquí reapareix la sal de 02-04: un valor únic per usuari, aleatori (CSPRNG), no secret, que es desa junt amb el hash i es barreja amb la contrasenya abans de fer el hash.

  • El que arregla: mata les rainbow tables (caldria precalcular una taula per cada sal possible) i fa que contrasenyes iguals donin hashes diferents. L'atac ja no és "tota la taula de cop": és fila a fila.
  • El que NO arregla: la velocitat. L'atacant ja no precalcula, però continua provant RockYou i els seus derivats contra cada fila a milers de milions d'intents per segon. sha256(sal + contrasenya) amb GPU continua caient a manta. (I compte: continuaria sent la construcció amb length extension de 03-02 — segona raó per no fabricar-la a mà.)

Etapa 3: funcions deliberadament lentes

La conclusió a què la indústria va trigar dècades a arribar: contra secrets de baixa entropia, l'única palanca del defensor és el cost de cada intent. Si verificar una contrasenya costa ~100 ms i una bona ració de RAM a propòsit, l'usuari legítim ni ho nota (un inici de sessió per sessió), però l'atacant que necessita milers de milions d'intents veu el seu pressupost multiplicat per milions. És exactament l'asimetria de les KDF lentes de 02-04, ara aplicada a verificadors. I amb la memòria com a arma anti-GPU: una GPU té milers de nuclis, però no milers de gigabytes de RAM ràpida per donar-los-hi.

flowchart LR
    A["Etapa 0<br/>text en clar"] -->|"hash 'unidireccional'"| B["Etapa 1<br/>SHA-256 sense sal"]
    B -->|"rainbow tables"| C["Etapa 2<br/>SHA-256 + sal"]
    C -->|"GPUs: continua sent rapid"| D["Etapa 3<br/>lent a proposit:<br/>bcrypt / scrypt / Argon2id"]

Les funcions correctes: bcrypt, scrypt i Argon2id

Les tres candidates serioses, dues de les quals ja coneixes de 02-04:

  • bcrypt (1999): el veterà honorable. Cost purament de CPU regulat per un factor de cost exponencial (cost 12 ≈ 4096 iteracions internes del seu nucli; cada +1 duplica el temps). Continua sent raonable, amb dos peròs: no és memory-hard (les GPU li van menjant terreny any rere any) i té una peculiaritat perillosa: només processa els primers 72 bytes de la contrasenya — tot el que passi d'aquí s'ignora en silenci, i més d'una implementació ho ha "resolt" truncant o pre-fent-hash malament. Si heretes bcrypt, no és una emergència; per a codi nou hi ha una opció millor.
  • scrypt (2009): el primer memory-hard, ja el vas fer servir per a la còpia de seguretat exportable amb n=2^17, r=8, p=1. Perfectament vàlid també com a password hasher; el seu punt feble és tenir els costos de CPU i memòria acoblats en un sol paràmetre principal.
  • Argon2id (2015): guanyador de la Password Hashing Competition i recomanació actual (també d'OWASP). Tres comandaments independents — memòria, iteracions (time cost), paral·lelisme — i la variant id combina resistència a atacs per GPU amb resistència a atacs de canal lateral. És la que adoptarà MediNube.
bcrypt scrypt Argon2id
Any / origen 1999 2009 2015 (guanyador PHC)
Memory-hard No
Paràmetres factor de cost n, r, p (acoblats) memòria, temps, paral·lelisme
Límit d'entrada 72 bytes No No
En Python paquet bcrypt pyca/cryptography, hashlib argon2-cffi
Veredicte Acceptable si ja el fas servir Bona opció Recomanada per a codi nou

Paràmetres orientatius d'Argon2id per a un servidor d'aplicacions el 2026 (i punt de partida d'OWASP): memòria 64 MiB, 3 iteracions, paral·lelisme 4, sal de 16 bytes, sortida de 32. La regla pràctica: apunta a 50–200 ms per verificació en el teu maquinari de producció i puja la memòria tant com el teu servidor toleri sota càrrega d'inicis de sessió concurrents. Són justament els valors per defecte actuals d'argon2-cffi, cosa que fa el codi que segueix molt poc dramàtic.

Argon2id en Python amb argon2-cffi

pip install argon2-cffi (el vam presentar a 02-04; ara el fem servir de veritat). La peça central és PasswordHasher:

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()  # Argon2id, 64 MiB, t=3, p=4 - defaults sans

# Registre: fer el hash
hash_desat = ph.hash("CavallCorrecteBateriaGrapa")
print(hash_desat)
# $argon2id$v=19$m=65536,t=3,p=4$YWxndW5zYWx0cnJyYQ$G3Kx0...

# Inici de sessio: verificar
try:
    ph.verify(hash_desat, "CavallCorrecteBateriaGrapa")   # OK: retorna True
    ph.verify(hash_desat, "contrasenya-incorrecta")         # llanca excepcio
except VerifyMismatchError:
    print("Contrasenya incorrecta")

Desglossament, peça a peça:

  • PasswordHasher() fixa els paràmetres de la política actual. Els pots ajustar (PasswordHasher(memory_cost=131072, time_cost=3, parallelism=4), memòria en KiB), però no els baixis dels defaults sense una raó mesurada.
  • ph.hash(contrasenya) genera ell sol una sal fresca de 16 bytes del CSPRNG i retorna el hash complet en format text. Fixa't: no li passem cap sal — impossible oblidar-la o reutilitzar-la. Accepta str directament (s'encarrega de l'UTF-8).
  • ph.verify(hash, contrasenya) torna a executar Argon2id amb la sal i els paràmetres que llegeix del propi hash i compara en temps constant. Amb una contrasenya incorrecta llança VerifyMismatchError — el patró de "excepció, no booleà silenciós" que ja vas veure a HistorialManipulat (02-03): una fallada de verificació ha de ser impossible d'ignorar per accident.
  • Cada crida a hash amb la mateixa contrasenya dona un resultat diferent (sal nova). Per això no es pot "cercar el hash a la BD" com fèiem amb els tokens a 03-02: aquí es cerca la fila pel nom d'usuari i es verifica el seu hash. Compara-ho amb la taula verificador vs clau: això és un verificador autocontingut, no una clau reproduïble.

El format modular: el hash que ho porta tot a dins

Aquest galimaties amb $ és el format modular (PHC string format), i entendre'l evita tot un gènere d'errors:

$argon2id$v=19$m=65536,t=3,p=4$YWxndW5zYWx0cnJyYQ$G3Kx0...
    │        │        │                │              │
    │        │        │                │              └─ hash (Base64)
    │        │        │                └─ sal (Base64)
    │        │        └─ parametres: 65536 KiB, 3 iteracions, paral·lelisme 4
    │        └─ versio de l'algorisme
    └─ algorisme
  • La sal i els paràmetres viatgen dins del hash. No necessites columnes sal, iteracions, algorisme a la taula d'usuaris: una sola columna de text (~100 caràcters) ho conté tot. verify hi llegeix el que necessita.
  • Que la sal sigui visible no és una fallada: com vam fixar a 02-04, la sal és única i aleatòria, però pública. El que protegeix la contrasenya és el cost de la funció, no el secret de la sal.
  • Conseqüència potent: cada fila declara els seus paràmetres, així que en una mateixa taula poden conviure hashes antics (m=65536) i nous (m=131072), o fins i tot bcrypt ($2b$12$...) i Argon2id durant una migració. Això és criptoagilitat (regla 8) aplicada a contrasenyes, i és la base de l'apartat següent. La peça que ho tanca és ph.check_needs_rehash(hash): retorna True si el hash desat es va generar amb paràmetres més febles que la política actual del PasswordHasher.

Cas pràctic: registre i inici de sessió a MediNube, amb migració inclosa

Context complet del fil: el portal arrossega la taula usuaris de l'etapa 2 — SHA-256 amb sal, el clàssic heretat. El codi trobat:

# codi heretat - NO fer servir
import hashlib, os

def desar_contrasenya_MALAMENT(usuari: str, contrasenya: str):
    sal = os.urandom(16)
    h = hashlib.sha256(sal + contrasenya.encode("utf-8")).hexdigest()
    bd.desar(usuari, esquema="sha256", sal=sal.hex(), hash=h)

def iniciar_sessio_MALAMENT(usuari: str, contrasenya: str) -> bool:
    fila = bd.cercar(usuari)
    h = hashlib.sha256(bytes.fromhex(fila.sal) + contrasenya.encode()).hexdigest()
    return h == fila.hash    # a mes, comparacio no constant

Diagnòstic ja conegut: sal correcta, però funció ràpida (etapa 2) i comparació amb ==. El repte real no és escriure la versió bona — és migrar sense reiniciar la contrasenya de tots els usuaris. No podem convertir els hashes nosaltres (no tenim les contrasenyes, de això es tracta!), però hi ha un moment en què cada contrasenya passa legítimament pel servidor: l'inici de sessió. L'estratègia estàndard és migrar en aquest moment, usuari a usuari:

# medinube/auth.py - versio correcta, amb migracio transparent
import hashlib, hmac
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()  # politica actual: Argon2id, 64 MiB, t=3, p=4

def registrar(usuari: str, contrasenya: str):
    """Alta d'un usuari nou: directament Argon2id."""
    bd.desar(usuari, esquema="argon2", hash=ph.hash(contrasenya))

def iniciar_sessio(usuari: str, contrasenya: str) -> bool:
    fila = bd.cercar(usuari)
    if fila is None:
        ph.hash(contrasenya)   # gastar el mateix temps encara que no existeixi
        return False           # (no delatar per timing que l'usuari no existeix)

    if fila.esquema == "sha256":                     # hash heretat (etapa 2)
        h = hashlib.sha256(bytes.fromhex(fila.sal) + contrasenya.encode()).hexdigest()
        if not hmac.compare_digest(h, fila.hash):
            return False
        # Contrasenya correcta i en memoria: MIGRAR ara
        bd.desar(usuari, esquema="argon2", hash=ph.hash(contrasenya), sal=None)
        return True

    # Esquema modern
    try:
        ph.verify(fila.hash, contrasenya)
    except VerifyMismatchError:
        return False
    if ph.check_needs_rehash(fila.hash):             # politica endurida?
        bd.desar(usuari, esquema="argon2", hash=ph.hash(contrasenya))
    return True

Llegeix-ho amb calma, perquè condensa mitja lliçó:

  • registrar: tres línies. Tota la complexitat (sal, paràmetres, format) viu a argon2-cffi. Res a inventar (regla 1). La política de qualitat de la contrasenya (longitud mínima, no estar en llistes de filtrades) és de la capa d'aplicació i complementa, no substitueix, això.
  • Usuari inexistent: s'executa igualment un hash perquè la resposta trigui el mateix que amb un usuari real — si no, el temps de resposta delataria quins noms d'usuari existeixen (el mateix esperit de la regla 6 a nivell d'aplicació).
  • Branca heretada: es verifica contra l'esquema antic (amb compare_digest, arreglant de pas el ==) i, si la contrasenya és correcta, s'aprofita que la tenim en memòria per re-fer el hash amb Argon2id i sobreescriure la fila. L'usuari no nota res; l'etiqueta esquema de la fila canvia per sempre. Amb el temps, la branca sha256 es buida — i per als rezagats que mai no tornen, es força un reinici per correu passat un termini (amb el token de recuperació ben desat de 03-02, tot connecta).
  • check_needs_rehash: la mateixa jugada, però cap al futur. Quan el 2029 pugeu el memory_cost, els hashes vells s'aniran renovant sols a cada inici de sessió. Criptoagilitat de sèrie, sense migracions traumàtiques.
  • El camp esquema explícit compleix el paper del byte de versió del nostre format v1 de 02-03: cada dada declara com està protegida.

Què NO fer (encara que sembli bona idea)

  • Limitar la longitud de la contrasenya ("màxim 16 caràcters"). No hi ha cap raó criptogràfica: el hash produeix sortida fixa pesi el que pesi l'entrada. Un límit curt només retalla l'entropia de l'usuari diligent. Un límit generós anti-DoS (p. ex. 1024 caràcters) sí que és raonable. I si fas servir bcrypt, recorda el seu truncament silenciós a 72 bytes: és un motiu per a Argon2id, no per prohibir frases de pas llargues.
  • Truncar o "normalitzar" la contrasenya (retallar espais, passar a minúscules "per usabilitat"). Cada transformació fon entropia i crea classes d'equivalència que l'atacant explota gratis.
  • El "pepper" mal entès. Un pepper és un secret global del servidor que s'afegeix al càlcul (típicament HMAC(pepper, contrasenya) abans del hash lent), desat fora de la BD — en el gestor de secrets que veurem a 06-01. Ben fet, afegeix una capa real: la BD robada sense el pepper no és atacable ni per diccionari. Però té riscos seriosos que el deixen en "opcional per a equips madurs": si el pepper es perd, tots els inicis de sessió moren sense recuperació possible; rotar-lo és difícil (no pots recalcular els hashes sense les contrasenyes); i mal implementat (concatenar en lloc d'HMAC, desar-lo a la mateixa BD) és teatre. Argon2id primer; pepper, només amb gestió de secrets solvent.
  • Compondre esquemes casolans: md5(sha1(contrasenya)), "el meu Argon2 però amb el meu XOR abans", doble Argon2 "per a més seguretat". Regla d'or 1, una vegada més: les combinacions casolanes no sumen forces, sumen superfície d'error. PasswordHasher tal qual.
  • Xifrar les contrasenyes en lloc de fer-ne el hash. Xifrar és reversible per disseny: qui tingui la clau (o la robi junt amb la BD) recupera totes les contrasenyes. Un verificador no ha de poder revertir-se ni pel mateix MediNube — regla d'or 7: l'objectiu aquí és verificar, no recuperar.

Més enllà de la contrasenya: MFA i passkeys

Tanquem amb honestedat sobre els límits: Argon2id protegeix la contrasenya a la teva base de dades; no pot fer res si l'usuari la teclea en un portal de phishing o la reutilitza en un lloc ja filtrat. Per això l'estat de l'art hi afegeix capes: MFA (un segon factor: TOTP, codis d'un sol ús) i, cada vegada més, passkeys (WebAuthn) — credencials basades en signatures de clau pública on el servidor ja no desa cap secret verificable per diccionari. No les desenvoluparem en aquest curs, però fixa't en el detall: passkeys = signatures digitals, la criptografia asimètrica que comença al mòdul que ve. Per a MediNube, amb dades sanitàries, l'MFA per al personal clínic no seria opcional en un desplegament real.

Errors Comuns i Consells

  • "Faig servir SHA-256 amb sal, ja estic a l'etapa bona". No: la sal només mata el precàlcul. Sense cost per intent continues a l'etapa 2, i les GPU la mastega. La pregunta de control: "quant triga a verificar-se una contrasenya?" — si la resposta és "res", aquest és el problema.
  • Desar la sal en una columna a part i passar-la a argon2-cffi. Senyal d'estar barallant-se contra la llibreria: hash() genera la sal i la incrusta en el format modular; verify() la llegeix d'allà. Si el teu esquema de BD té columna sal junt amb hashes Argon2, alguna cosa s'està fent a mà que no tocava.
  • Retornar errors diferents per a "l'usuari no existeix" i "contrasenya incorrecta" (o trigar de manera diferent). Enumeració d'usuaris servida en safata. Mateix missatge, mateix temps.
  • Ajustar els paràmetres a ull o copiar-los d'un tutorial del 2015. Mesura'ls en el teu maquinari de producció apuntant a 50–200 ms, revisa la política cada pocs anys i deixa que check_needs_rehash propagui la pujada.
  • Migrar "quan hi hagi temps" deixant iniciar_sessio_MALAMENT en producció. La migració a l'inici de sessió costa una tarda de feina i es paga sola a la primera bretxa. Prioritat alta, de veritat.
  • Consell: afegeix al teu registre (i als teus tests) contrasenyes del top de RockYou i verifica que la teva capa d'aplicació les rebutja. El millor Argon2id no salva medinube2026 d'un diccionari de deu mil entrades.

Exercicis

Exercici 1. La taula d'usuaris d'un servei filtrat conté aquestes tres files (mateixa columna hash). Indica en quina etapa de l'evolució està cada esquema, quin atac les afecta i ordena-les de pitjor a millor:

alice  | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
bob    | a1b2c3d4e5f6a7b8:9c87b1cbb62d5a7ac43276b8b12ab5a4adcb1e83e5c2e6f1...
carol  | $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub...

Exercici 2. Sense executar-lo, raona què imprimeix cada línia i per què:

from argon2 import PasswordHasher
ph = PasswordHasher()
h1 = ph.hash("gat-blau-7")
h2 = ph.hash("gat-blau-7")
print(h1 == h2)
print(ph.verify(h1, "gat-blau-7"), ph.verify(h2, "gat-blau-7"))
ph2 = PasswordHasher(memory_cost=131072)
print(ph2.check_needs_rehash(h1))

Exercici 3. MediNube absorbeix una clínica el sistema de la qual desava contrasenyes amb bcrypt, factor de cost 10 (format $2b$10$..., paquet bcrypt: bcrypt.checkpw(contrasenya_bytes, hash_bytes)). Escriu la branca addicional de la funció iniciar_sessio que verifiqui aquests hashes i els migri a Argon2id en el primer inici de sessió correcte. Cal una taula nova o una columna nova de sal?

Solucions

Solució 1. alice: 64 hex = SHA-256 a seques, etapa 1 (de fet és sha256("password")): vulnerable a rainbow tables i a diccionari a màxima velocitat; els hashes repetits entre usuaris delaten contrasenyes compartides. bob: format sal:hash, etapa 2 (hash ràpid amb sal): immune al precàlcul, però cada fila cau a ritme de GPU amb un diccionari. carol: format modular Argon2id, etapa 3: cada intent costa 64 MiB i ~desenes de ms també a l'atacant. De pitjor a millor: alice < bob < carol. Detall d'examen: la sal visible a les files de bob i carol no és una debilitat.

Solució 2. h1 == h2False: cada hash() genera una sal fresca del CSPRNG, així que la mateixa contrasenya produeix hashes diferents (i això és desitjable: no delata contrasenyes repetides). Tots dos verifyTrue True: cada hash porta la seva sal a dins, i la verificació torna a derivar amb ella. ph2.check_needs_rehash(h1)True: h1 declara m=65536 en el seu format modular, per sota de la política de ph2 (131072 KiB); en un inici de sessió real això dispararia el re-hash transparent.

Solució 3. N'hi ha prou amb una branca més; ni taula ni columna de sal — bcrypt també fa servir format modular amb la sal a dins ($2b$10$<sal><hash>):

import bcrypt

    # ... dins de iniciar_sessio(), despres de cercar la fila:
    if fila.esquema == "bcrypt":
        if not bcrypt.checkpw(contrasenya.encode("utf-8"), fila.hash.encode("utf-8")):
            return False
        # correcta: migrar a la politica actual de MediNube
        bd.desar(usuari, esquema="argon2", hash=ph.hash(contrasenya))
        return True

Punts a valorar: checkpw exigeix bytes (d'aquí els .encode), ja compara en temps constant internament, i la migració reaprofita el moment en què la contrasenya està legítimament en memòria. Recordatori del límit de bcrypt: si el portal permet frases de pas de més de 72 bytes, els hashes $2b$ heretats només van verificar els primers 72 — un motiu més per completar la migració (Argon2id no trunca).

Conclusió

El deute més antic del curs està saldat: el portal de MediNube desa verificadors Argon2id ($argon2id$..., amb sal i paràmetres a dins), registra amb tres línies de PasswordHasher, verifica amb verify + VerifyMismatchError, migra els SHA-256 heretats al primer inici de sessió i es re-endureix sola amb check_needs_rehash. I ha quedat clara la distinció amb 02-04: mateixa família de funcions lentes, però allà la sortida era una clau que xifra i aquí un verificador que només comprova.

Amb això es tanca el Mòdul 3 complet, i les tres peces encaixen en una escala: el hash (03-01) dona integritat quan l'empremta viu a salvo — exports verificables, deduplicació, git; l'HMAC (03-02) afegeix una clau compartida i dona autenticitat — webhooks signats amb X-MediNube-Firma, tokens de recuperació desats com a sha256(token); i Argon2id (03-03) inverteix la virtut de la velocitat per protegir l'únic secret que no podem triar fort: les contrasenyes humanes. Integritat, autenticitat, credencials: les tres sense xifrar ni un sol byte.

Però repassa la lletra petita de tot l'arsenal que MediNube té fins avui: AES-GCM necessita que emissor i receptor ja comparteixin la clau; HMAC, també; fins i tot el pepper era un altre secret compartit. Tot el nostre edifici descansa en una suposició que portem aparcant des de 02-01: que les claus van arribar a tots dos costats per art de màgia. Com acorden una clau MediNube i una clínica nova que mai no s'han vist, parlant per una xarxa on qualsevol escolta? I com signa un metge una recepta de manera que qualsevol — no només qui comparteix una clau — la pugui verificar, amb no repudi inclòs? Aquest és el salt conceptual més gran del curs: la criptografia asimètrica, on les claus vénen en parells i la meitat es publica als quatre vents. Comença al Mòdul 4 amb la lliçó 04-01: Fonaments de Clau Pública i RSA. Ens veiem a l'altre costat.

© Copyright 2026. Tots els drets reservats