La lliçó anterior va acabar amb una pregunta oberta sobre les claus públiques, però també va deixar una peça a mitges: en signar un rebut de Nimbus no se signava el missatge, se signava el seu hash. Aquesta primitiva va apareixent des del primer mòdul —a l'empremta de les còpies, a l'HMAC del webhook de la passarel·la, al TOTP, a la taula de credencials— i fins ara ningú no l'ha explicat. Aquesta lliçó la desenvolupa sencera: què és una funció hash criptogràfica i quines propietats té, per què MD5 i SHA-1 estan trencats, per què un hash a seques no autentica res i d'aquí neix l'HMAC, i —el cor de la lliçó i un deute explícit del mòdul 2— com ha de desar Nimbus les contrasenyes dels seus usuaris. És probablement la lliçó més directament aplicable de tot el mòdul: gairebé qualsevol sistema que construeixis desarà contrasenyes, i fer-ho malament és la diferència entre una filtració molesta i una catàstrofe per a milers de persones.

Contingut

  1. Què és una funció hash criptogràfica
  2. Propietats i efecte allau
  3. Família d'algorismes: què fer servir avui i què no
  4. Usos legítims del hash
  5. Per què un hash no autentica: del hash al MAC
  6. HMAC i el webhook de la passarel·la de pagament de Nimbus
  7. Per què SHA-256 és inacceptable per a contrasenyes
  8. Sal i pepper
  9. Funcions de derivació lenta: Argon2id, scrypt, bcrypt i PBKDF2
  10. Registre i verificació de contrasenyes a Nimbus
  11. Enumeració d'usuaris i verificació en temps constant
  12. Què fer si es filtra la base de dades de credencials
  13. Sumes de verificació no criptogràfiques

  1. Què és una funció hash criptogràfica

Una funció hash pren una entrada de qualsevol mida i produeix una sortida de mida fixa, anomenada resum, digest o empremta.

"hola"                         --SHA-256-->  32 bytes
Un fitxer de 8 GB              --SHA-256-->  32 bytes
Una cadena buida               --SHA-256-->  32 bytes

La clau és a l'adjectiu criptogràfica: qualsevol pot escriure una funció que redueixi dades a un nombre fix —això és una taula hash de tota la vida—, i el que en converteix una en criptogràfica són les garanties que ofereix davant d'un adversari que intenta manipular-la deliberadament. I hi ha una diferència essencial amb tot el que hem vist fins ara: una funció hash no té clau. Qualsevol pot calcular el hash de qualsevol cosa, cosa que la fa útil per verificar integritat davant d'errors i completament insuficient per autenticar davant d'un atacant, com veurem a l'apartat 5.


  1. Propietats i efecte allau

Propietat Què significa Per què importa
Determinista La mateixa entrada produeix sempre la mateixa sortida Sense això no es pot verificar res
Ràpida de calcular Gigabytes per segon Permet calcular el hash de fitxers grans. I és un problema per a les contrasenyes (apartat 7)
Unidireccional (resistència a preimatge) Donat h, és inviable trobar m tal que hash(m) = h Permet desar empremtes en lloc de dades: ningú no inverteix la teva empremta
Resistència a segona preimatge Donat m1, inviable trobar m2 ≠ m1 amb el mateix hash Ningú no substitueix el teu document per un altre que quadri
Resistència a col·lisió Inviable trobar qualsevol parell m1 ≠ m2 amb el mateix hash És la propietat més difícil de mantenir i la primera que cau
Efecte allau Canviar un bit de l'entrada canvia ~la meitat dels bits de la sortida Impedeix deduir res sobre l'entrada a partir de la sortida

Les dues últimes mereixen atenció. La resistència a col·lisió és més feble que la de segona preimatge perquè l'atacant pot triar els dos missatges: per la paradoxa de l'aniversari, amb una sortida de n bits es troben col·lisions en unes 2^(n/2) operacions, no 2^n. Per això SHA-256, amb 256 bits de sortida, ofereix 128 bits de seguretat davant de col·lisions. I per això SHA-1, amb 160 bits, n'oferia només 80 —i va caure—.

L'efecte allau es veu millor del que s'explica:

import hashlib

def sha256_hex(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

a = "Reserva 88421 confirmada per al 14/08/2026 a les 10:30"
b = "Reserva 88421 confirmada per al 14/08/2026 a les 10:31"   # UN caracter

ha, hb = sha256_hex(a), sha256_hex(b)
print("A:", ha)
print("B:", hb)

# Comptem quants BITS difereixen entre els dos resums
ba = int(ha, 16)
bb = int(hb, 16)
diferents = bin(ba ^ bb).count("1")      # XOR: 1 alla on els bits difereixen
print(f"Bits diferents: {diferents} de 256 ({diferents / 256:.0%})")

Sortida:

A: cb0dd2aebb5de43d807bf453db9af90497fe05c429e4835469c1d12d1b1579ba
B: a6a761ff0ce732fcf941faf5b437abb6ea1866dfffd2085ba465a7a1dd208f4c
Bits diferents: 142 de 256 (55%)

El que cal entendre: un sol caràcter de diferència —el minut de la reserva— i els dos resums no s'assemblen gens. ba ^ bb fa un XOR entre els dos enters, posant un 1 allà on difereixen, i comptar els uns dona la distància de Hamming. El resultat ronda sempre el 50 %, que és el que s'espera d'una sortida indistingible de l'atzar: si un hash produís un 5 % de canvi, revelaria informació sobre l'entrada i seria inservible. (hexdigest() retorna 64 caràcters hexadecimals, que són els 32 bytes de sortida; digest() retorna els bytes crus.)


  1. Família d'algorismes: què fer servir avui i què no

Algorisme Sortida Estat el 2026 Fer-lo servir avui?
MD5 128 bits Trencat. Col·lisions en segons en un portàtil Mai per a seguretat
SHA-1 160 bits Trencat. Col·lisió pràctica demostrada (SHAttered, 2017) Mai per a seguretat
SHA-256 / SHA-512 256 / 512 bits Sòlid. Família SHA-2 Sí. Opció per defecte
SHA-3 (SHA3-256…) Variable Sòlid, construcció diferent (esponja) Sí, quan es vol diversitat de disseny
BLAKE2 / BLAKE3 Variable Sòlid i més ràpid que SHA-2 Sí, per a integritat de gran volum
CRC32 32 bits No és criptogràfic Només detecció d'errors de transmissió (apartat 13)

Per què MD5 i SHA-1 estan trencats. No ho estan en preimatge —ningú no inverteix un hash SHA-1—, sinó en resistència a col·lisió: es poden construir dos documents diferents amb el mateix resum. MD5 va caure el 2004 i avui es generen col·lisions en segons; es va demostrar fins i tot la construcció de dos certificats digitals diferents amb el mateix hash, cosa que permetia suplantar identitats. SHA-1 va caure públicament el 2017 amb l'atac SHAttered, que va produir dos PDF diferents amb el mateix resum, i el 2020 els atacs de col·lisió amb prefix triat van abaixar el cost a desenes de milers d'euros de còmput, cosa que va portar a la seva retirada definitiva de certificats i signatures.

Per què una col·lisió és tan greu. Recorda de l'apartat 8 de 03-03 que una signatura es calcula sobre el hash. Si un atacant fabrica dos documents amb el mateix resum —un d'innocu, un altre de maliciós—, aconsegueix que li signis l'innocu i aquella mateixa signatura és vàlida per al maliciós. La signatura és correcta; el hash és el que va trair.

Avís pràctic: MD5 i SHA-1 continuen apareixent en codi heretat per a coses que no són seguretat —claus de memòria cau, deduplicació interna, identificadors de recursos—. En aquests usos no hi ha risc, però convé documentar-ho explícitament perquè un auditor no ho assenyali i, sobretot, perquè ningú no els reutilitzi després amb intenció de seguretat.


  1. Usos legítims del hash

Ús Exemple a Nimbus
Verificar integritat davant d'errors Comprovar que la còpia nocturna es va transferir completa
Empremta de fitxer i deduplicació Identificar sense comparar byte a byte; no desar dues vegades el mateix adjunt
Índex de dades xifrades Cercar sense desxifrar, amb HMAC determinista (03-07; requereix clau)
Base de signatures i MAC El que se signa és el hash (03-03 i apartat 6)
Identificar versions Cada commit de Git és un hash

El cas més quotidià per a la Lucía és verificar una còpia:

# 1. En crear la copia, es genera la seva empremta
sha256sum copia-2026-08-02.tar.gz > copia-2026-08-02.tar.gz.sha256

# 2. Mesos despres, despres de descarregar-la de l emmagatzematge extern,
#    es verifica ABANS d intentar restaurar-la
sha256sum -c copia-2026-08-02.tar.gz.sha256
9f4c1a7d3e0b8256cf10a94b7d3e6f28c0574ab19e83d2f6410c9b7e5a2d8f31  copia-2026-08-02.tar.gz
copia-2026-08-02.tar.gz: La suma coincideix

sha256sum -c llegeix el fitxer d'empremtes i recalcula el hash de l'arxiu real; si no coincideix, imprimeix LA SUMA NO COINCIDEIX i retorna un codi de sortida diferent de zero, cosa que permet automatitzar la comprovació dins de l'script de restauració. Això detecta corrupció —transferència truncada, disc amb sectors malmesos, fitxer mal descomprimit— i és exactament el «0» de l'esquema 3-2-1-1-0 de 02-04, zero errors de verificació. El que no detecta és un atac, i aquest és el punt que enllaça amb l'apartat següent.


  1. Per què un hash no autentica: del hash al MAC

Torna a l'exemple anterior amb ulls d'atacant. Un intrús amb accés d'escriptura a l'emmagatzematge de còpies substitueix copia-2026-08-02.tar.gz per una versió manipulada, executa sha256sum sobre la seva versió i sobreescriu el fitxer .sha256 amb el nou resultat. La verificació de la Lucía dirà «la suma coincideix». I coincidirà. La fallada no és a SHA-256: és que el hash no té clau, així que qualsevol el pot recalcular.

La regla: un hash protegeix davant d'accidents; un MAC o una signatura protegeixen davant d'adversaris. Si l'atacant pot modificar la dada, també en pot modificar el hash.

Les dues solucions ja les coneixes: el MAC (HMAC), amb clau compartida, quan les dues parts es coneixen i no cal prova davant de tercers —el que desenvolupa aquest apartat—; i la signatura digital, amb parell asimètric, quan el verificador pot ser qualsevol o cal no-repudi (03-03).

Per què no n'hi ha prou amb hash(clau || missatge)

La construcció ingènua consisteix a concatenar la clau davant del missatge i calcular-ne el hash. Sembla raonable: sense la clau no es pot calcular. I és insegura amb les funcions de la família SHA-2, per l'atac d'extensió de longitud.

La raó és en com funcionen internament: SHA-256 processa el missatge per blocs i la seva sortida és el seu estat intern final. Un atacant que coneix hash(clau || missatge) i la longitud de la clau pot reprendre la funció des d'aquell estat i calcular hash(clau || missatge || farciment || dades_afegides) sense conèixer la clau. Obté un MAC vàlid per a un missatge que ell ha estès.

Traduït a Nimbus: si l'esquema de les URL signades fes servir aquesta construcció, un atacant podria partir d'una URL signada legítima i afegir-hi paràmetres —per exemple, ampliar la caducitat o canviar l'abast—, i generar una signatura vàlida sense la clau.

L'HMAC existeix precisament per a això. La seva construcció fa servir la clau dues vegades, en dues passades imbricades:

HMAC(K, m) = H( (K XOR opad) || H( (K XOR ipad) || m ) )

La passada exterior impedeix reconstruir l'estat intern, i amb això l'atac d'extensió. No l'implementis tu: hmac.new(...) en Python ja ho fa.


  1. HMAC i el webhook de la passarel·la de pagament de Nimbus

La passarel·la de pagament notifica a Nimbus quan un cobrament es completa, mitjançant una petició HTTP a un extrem públic. Sense verificació, qualsevol que descobrís aquella URL podria enviar notificacions falses de pagament —cosa que, en un SaaS de reserves, es tradueix en cites confirmades sense cobrar—. La passarel·la i Nimbus comparteixen un secret i signen cada notificació amb HMAC-SHA256.

import hmac, hashlib, time
from fastapi import APIRouter, Request, HTTPException

router = APIRouter()
SECRET_WEBHOOK: bytes = obtenir_secret("passarela/webhook")     # gestor (03-06)
TOLERANCIA_SEGONS = 300                                          # 5 minuts

def signatura_esperada(cos: bytes, marca: str) -> str:
    """Se signa marca-de-temps + cos CRU, no el JSON reserialitzat."""
    return hmac.new(SECRET_WEBHOOK,
                    marca.encode("ascii") + b"." + cos,
                    hashlib.sha256).hexdigest()

@router.post("/webhooks/pasarela")
async def rebre_webhook(peticio: Request):
    cos = await peticio.body()              # 1. cos CRU, tal com va arribar
    marca = peticio.headers.get("X-Pasarela-Timestamp", "")
    rebuda = peticio.headers.get("X-Pasarela-Signature", "")
    if not marca or not rebuda:
        raise HTTPException(400, "Falten capcaleres de signatura")

    # 2. FRESCOR: es rebutgen les notificacions velles (anti-repeticio)
    try:
        if abs(time.time() - int(marca)) > TOLERANCIA_SEGONS:
            raise HTTPException(400, "Notificacio caducada")
    except ValueError:
        raise HTTPException(400, "Marca de temps no valida")

    # 3. COMPARACIO EN TEMPS CONSTANT
    if not hmac.compare_digest(signatura_esperada(cos, marca), rebuda):
        registrar_esdeveniment("webhook.signatura_invalida", ip=peticio.client.host)
        raise HTTPException(401, "Signatura no valida")

    # 4. IDEMPOTENCIA: la mateixa notificacio pot arribar dues vegades
    esdeveniment = parsejar(cos)
    if ja_processat(esdeveniment["id"]):
        return {"estat": "duplicat_ignorat"}
    processar_pagament(esdeveniment)
    return {"estat": "ok"}

Anàlisi de les decisions, que són totes deliberades:

  • Se signa el cos cru (await peticio.body()), no el diccionari reserialitzat. Si parseges el JSON i el tornes a serialitzar, qualsevol diferència d'espaiat o d'ordre de claus canvia els bytes i la signatura falla. És el mateix problema de canonicalització de 03-03, i la solució és signar exactament el que va viatjar per la xarxa.
  • Se signa també la marca de temps, unida al cos amb un separador. Si només es signés el cos, un atacant podria capturar una notificació legítima i reenviar-la demà amb la signatura intacta. Aquesta és la propietat de frescor de 03-01, i la finestra de cinc minuts és el que la garanteix; el mateix raonament que hi ha darrere de la finestra de 30 segons del TOTP de 02-05.
  • hmac.compare_digest compara en temps constant: triga el mateix encerti el primer caràcter o l'últim. Amb ==, Python tallaria a la primera diferència i un atacant que mesurés milers d'intents reconstruiria la signatura caràcter a caràcter (03-01). Aquesta línia, tota sola, és la diferència entre un webhook segur i un de vulnerable. Compte: els dos arguments han de ser del mateix tipus (aquí, hexdigest() a banda i banda).
  • Es registra l'intent fallit, amb la IP i sense bolcar el cos (02-04): una ratxa de signatures no vàlides és una alerta. I la idempotència, que no és criptografia però sí imprescindible, perquè les passarel·les reintenten i processar dues vegades el mateix pagament és un incident de negoci.

Aquest mateix patró —HMAC sobre un missatge que inclou una caducitat— és exactament el que hi ha darrere de les URL signades de 120 segons del bucket A-05 que anem citant des del mòdul 1. Es desmunta per dins a 03-07.


  1. Per què SHA-256 és inacceptable per a contrasenyes

Arribem al cor de la lliçó. Nimbus desa credencials dels seus usuaris, i aquestes persones —amb tota seguretat— reutilitzen les seves contrasenyes en altres serveis. Desar-les malament no només compromet Nimbus: compromet el correu, el banc i la vida digital de milers de pacients i professionals.

Comencem pel que mai no es fa:

Desar-les en clar és indefensable: qualsevol filtració, còpia de seguretat o SELECT les exposa. Xifrar-les tampoc no val, perquè existeix una clau que les desxifra i qui roba la base de dades sol poder robar també la clau —i, a més, mai no cal recuperar una contrasenya, només verificar-la—. I md5(contrasenya) suma a tot l'anterior un algorisme trencat.

El problema amb sha256(contrasenya) és que SHA-256 és ràpid. Aquesta virtut, que el fa excel·lent per verificar una còpia de 8 GB, és exactament el defecte que l'inhabilita aquí:

Atac Cost amb SHA-256 a seques
Força bruta amb GPU Una GPU moderna calcula de l'ordre de 10.000 milions de SHA-256 per segon. Un diccionari de les 10.000 milions de contrasenyes més comunes s'esgota en un segon
Taules rainbow Taules precalculades que inverteixen hashos comuns a l'instant, disponibles públicament
Hashos idèntics Sense sal, dos usuaris amb la mateixa contrasenya tenen el mateix hash: es veu d'un cop d'ull qui comparteix contrasenya, i trencar-ne una les trenca totes

Un càlcul que ajuda a interioritzar-ho: una contrasenya de vuit caràcters alfanumèrics té unes 218 bilions de combinacions, que a 10.000 milions per segon s'esgoten en menys de sis hores amb una sola GPU; amb Argon2id ben calibrat, aquesta mateixa cerca passaria d'hores a segles, perquè cada intent costa centenars de milers de vegades més. D'aquí surten les tres peces de la solució: sal (que mata les taules precalculades i els hashos idèntics), lentitud deliberada (que encareix cada intent) i cost de memòria (que anul·la l'avantatge de les GPU).


  1. Sal i pepper

La sal (salt) és un valor aleatori, únic per usuari, que es combina amb la contrasenya abans de derivar el hash i es desa al seu costat, en clar.

Les quatre preguntes que sempre sorgeixen: no és secreta (es desa al costat del hash i no passa res); no pot ser la mateixa per a tothom ni derivar-se del correu o de l'ID, perquè ha de ser aleatòria i impredictible i els correus es repeteixen entre serveis; 16 bytes de secrets és la mida estàndard; i es desa dins de la mateixa cadena del hash, perquè els formats moderns ja la inclouen. Què aconsegueix exactament: anul·la les taules rainbow (una taula precalculada només serveix per a una sal concreta, així que se n'hauria de construir una per usuari), trenca la igualtat de hashos (dos usuaris amb la contrasenya Valencia2026 tenen hashos completament diferents) i obliga a atacar d'un en un, perquè sense sal un atacant prova cada candidat contra els 40.000 usuaris alhora.

El pepper (pebre) és un secret global de l'aplicació que s'afegeix a més de la sal, i la diferència crucial del qual és on viu:

Sal Pepper
Abast i secret Única per usuari, no secreta Única per a l'aplicació, secreta
On viu A la base de dades, al costat del hash Fora de la base de dades: gestor de secrets, HSM o variable del procés
Què aporta Anul·la taules i col·lisions Si roben només la base de dades, els hashos són inatacables

El pepper és una capa de defensa en profunditat valuosa —cobreix exactament l'escenari més freqüent, el bolcat de base de dades per una injecció SQL— però té un cost real: rotar-lo obliga que tots els usuaris canviïn la seva contrasenya o a un esquema de doble verificació, perquè no es pot recalcular sense les contrasenyes originals. Per a Nimbus, la recomanació és implantar primer Argon2id correctament i valorar el pepper després, en forma d'HMAC aplicat a la contrasenya abans de derivar-la, amb la clau al gestor de secrets.


  1. Funcions de derivació lenta: Argon2id, scrypt, bcrypt i PBKDF2

Aquestes funcions s'anomenen KDF de contrasenya (password-based key derivation function) i el seu disseny és deliberadament car. És la distinció que vam deixar anunciada a 03-02: HKDF deriva claus a partir de claus i és ràpid; aquestes deriven a partir de secrets de baixa entropia i han de ser lentes.

Algorisme Cost de memòria Paràmetres de partida el 2026 Recomanació
Argon2id Sí, configurable memòria 19 MiB, temps 2, paral·lelisme 1 (o 46 MiB / 1 / 1) Primera opció. Guanyador del Password Hashing Competition
scrypt N = 2^17, r = 8, p = 1 Bona alternativa si no hi ha Argon2
bcrypt Poc (4 KB fixos) cost 12 Acceptable. Limitat a 72 bytes d'entrada
PBKDF2-HMAC-SHA256 No 600.000 iteracions Només si una norma ho exigeix (per exemple FIPS). El més feble davant de GPU

Comprova sempre la guia vigent d'OWASP, perquè el maquinari avança. La clau és al cost de memòria: una GPU té milers de nuclis però poca memòria per nucli, així que un algorisme que exigeix 19 MB per càlcul impedeix que aquells milers de nuclis treballin en paral·lel i l'avantatge del maquinari especialitzat s'esvaeix. PBKDF2 només exigeix temps de CPU, que és justament el que les GPU i els ASIC fan barat.

Com calibrar-los. El criteri no és memoritzar nombres, és mesurar en el teu maquinari: fixa un pressupost de temps per verificació d'entre 250 i 500 ms en producció (menys deixa massa marge a l'atacant, més degrada l'experiència i obre una via de denegació de servei); puja primer la memòria, que és el que penalitza les GPU, i després el temps; mesura amb la concurrència real, perquè 20 inicis de sessió simultanis amb 46 MiB cadascun són gairebé 1 GB de RAM en pics, i això és un paràmetre de capacitat a més de seguretat; i anota valors i data per revisar-los cada any, sabent que el rehash transparent de l'apartat següent fa que pujar-los no costi res.


  1. Registre i verificació de contrasenyes a Nimbus

Esquema de la taula de credencials. Fixa't en el que no conté:

CREATE TABLE credencials (
    usuari_id        BIGINT      PRIMARY KEY REFERENCES usuaris(id),
    -- Cadena PHC completa: inclou algorisme, parametres, sal i hash.
    -- No hi ha columna "sal" separada: ja viatja a dins.
    hash_password    TEXT        NOT NULL,
    actualitzat_el   TIMESTAMPTZ NOT NULL DEFAULT now(),
    intents_fallits  SMALLINT    NOT NULL DEFAULT 0,   -- forca bruta (02-05)
    bloquejat_fins   TIMESTAMPTZ
);

-- El rol d informes (01-04) no hi te res a fer:
REVOKE ALL ON credencials FROM nimbus_informes;
GRANT SELECT, UPDATE ON credencials TO nimbus_api;

Una cadena PHC d'Argon2id té aquest aspecte:

$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHR2YWx1ZQ$3xTk9pQmR0aVdlbGxEb25lSGFzaFZhbHVl
 algorisme  ver  parametres      SAL aleatoria      hash derivat
                 (19456 KiB, 2 passades, 1 fil)     (tots dos en Base64)

Tot el que cal per verificar és en aquella cadena, i per això no cal una columna de sal ni una de paràmetres: quan canviïs els paràmetres, els hashos antics es continuaran verificant amb els seus. El codi, amb la llibreria argon2-cffi, que és la implementació de referència en Python:

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError

# Parametres centralitzats en UN sol lloc (apartat 9)
ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1)

# Hash "esquer": es fa servir quan l usuari NO existeix, perque la resposta
# trigui el mateix. Es calcula una vegada en arrencar el proces.
HASH_ESQUER = ph.hash("contrasenya-que-ningu-no-fara-servir-mai")

def registrar(usuari_id: int, password: str, repo) -> None:
    if len(password) < 12:                     # politica (02-05), ABANS de derivar
        raise ValueError("La contrasenya ha de tenir almenys 12 caracters")
    repo.desar_hash(usuari_id, ph.hash(password))   # la sal la genera la llibreria

def verificar(email: str, password: str, repo) -> bool:
    usuari = repo.buscar_per_email(email)
    # Si l usuari no existeix es verifica contra l esquer: cost identic,
    # l atacant no pot distingir (apartat 11).
    desat = usuari.hash_password if usuari else HASH_ESQUER
    try:
        ph.verify(desat, password)
    except (VerifyMismatchError, VerificationError, InvalidHashError):
        return False
    if usuari is None:                         # era l esquer: sempre falla
        return False
    # REHASH TRANSPARENT: si els parametres han canviat, es recalcula ara
    # que tenim la contrasenya en clar (unic moment possible).
    if ph.check_needs_rehash(desat):
        repo.desar_hash(usuari.id, ph.hash(password))
    return True

Explicació de les decisions no evidents:

  • ph.hash(password) genera la sal internament amb una font segura i retorna la cadena PHC completa. No hi ha cap decisió que puguis prendre malament.
  • ph.verify llança excepció en lloc de retornar False. Mateix criteri de disseny que verify a Ed25519 (03-03): obliga a tractar la fallada. VerifyMismatchError és contrasenya incorrecta; InvalidHashError significa que la cadena desada està corrupta o és d'un altre format, i mereix una alerta.
  • El hash esquer és la peça de l'apartat 11. Sense ell, la resposta per a un usuari inexistent seria immediata i per a un d'existent trigaria 300 ms: aquesta diferència és l'enumeració d'usuaris.
  • check_needs_rehash compara els paràmetres de la cadena desada amb els actuals. Si has pujat la memòria de 19 a 46 MiB, retorna True i el hash s'actualitza en l'únic instant en què disposes de la contrasenya en clar. Amb aquest mecanisme, endurir els paràmetres no requereix migració ni molestar ningú: la base d'usuaris s'actualitza sola a mesura que la gent entra.
  • La política de longitud es comprova abans de derivar, per no gastar 300 ms de CPU en una entrada ja no vàlida; i cal posar també un màxim raonable (128 caràcters), perquè sense ell algú podria enviar 10 MB de contrasenya i provocar una denegació de servei. I no registris mai la contrasenya, ni tan sols en un logger.debug temporal: és l'error de 02-04 i sobreviu als registres molt més del que ningú espera.

  1. Enumeració d'usuaris i verificació en temps constant

Un sistema enumera usuaris quan permet distingir si un compte existeix. Sembla menor i no ho és: és el pas previ del password spraying i del phishing dirigit de 02-03. Saber que [email protected] existeix converteix un atac genèric en un de personalitzat.

Les quatre fuites per les quals es filtra aquesta informació:

Fuita Símptoma Correcció
Missatge diferent «Usuari no trobat» davant de «Contrasenya incorrecta» Missatge únic: «Credencials incorrectes»
Temps diferent 5 ms si no existeix, 300 ms si existeix Hash esquer (apartat 10)
Codi o estructura de resposta 404 davant de 401, o camps diferents Resposta idèntica
Registre i recuperació «Aquest correu ja està registrat» «Si el correu està disponible, rebràs un missatge»

La del temps és la més oblidada perquè no es veu llegint el codi: cal mesurar-la, cronometrant cent intents amb un correu existent i cent amb un d'inventat; si les mitjanes difereixen de manera consistent, hi ha fuita.

I no confonguis dues coses que s'assemblen: el temps constant en la comparació (hmac.compare_digest) evita reconstruir un secret caràcter a caràcter, mentre que el temps constant en la resposta global (hash esquer) evita distingir si un compte existeix. Són defenses diferents contra atacs diferents i calen totes dues. Un matís honest: la igualtat perfecta de temps és inassolible en un sistema real amb memòries cau i xarxa; l'objectiu és que la diferència quedi molt per sota del soroll de mesura, no que sigui zero.


  1. Què fer si es filtra la base de dades de credencials

Escenari: la Lucía descobreix que un bolcat de la taula credencials ha sortit de la infraestructura. Amb Argon2id ben calibrat, les contrasenyes no són immediatament utilitzables, però això no significa que no hi hagi res a fer.

Primeres hores: (1) contenir i preservar —tallar l'accés utilitzat, conservar evidència i registres, segons el pla de resposta de 04-05—; (2) determinar l'abast exacte: quants usuaris, quines altres taules, quina finestra temporal; (3) invalidar totes les sessions i tokens actius, perquè una sessió vàlida no necessita contrasenya; (4) forçar el restabliment per un flux que no depengui de la contrasenya antiga.

Dies següents: (5) rotar el pepper, si existeix, i qualsevol secret que fos a la mateixa base de dades; (6) notificar en termini; (7) endurir els paràmetres d'Argon2id i forçar el rehash; (8) fer l'anàlisi de causa arrel amb el mètode de 02-06: no es busca culpable, es busca per què el control no hi era.

Nota de validació professional. L'RGPD obliga a notificar a l'autoritat de control en un termini màxim de 72 hores des del coneixement de la bretxa quan hi hagi risc per als drets dels afectats, i a comunicar-los-ho a aquests si el risc és alt. Com que les dades de Nimbus inclouen informació que revela indirectament l'estat de salut, l'anàlisi s'ha de fer amb el delegat o responsable de protecció de dades i amb assessoria jurídica. El detall normatiu es tracta a 06-03.

El que NO cal fer: minimitzar públicament («els hashos estan xifrats, no hi ha risc»), retardar la notificació esperant l'informe complet, o donar per segur que els usuaris no reutilitzen aquelles contrasenyes en altres serveis. Ho fan. Aquest és el motiu real pel qual Argon2id importa.


  1. Sumes de verificació no criptogràfiques

CRC32, Adler-32 i els dígits de control d'un IBAN o un DNI són sumes de verificació, no hashos criptogràfics. El seu propòsit és detectar errors accidentals de transmissió o de teclejat —i per a això són excel·lents—, però estan dissenyats davant del soroll, no davant d'un adversari: CRC32 produeix 32 bits de sortida i construir una col·lisió a voluntat és trivial, mentre que en SHA-256, amb 256 bits, és inviable. Ús correcte de CRC32: Ethernet, ZIP, PNG, detecció de corrupció. Ús correcte de SHA-256: integritat, signatures i empremtes.

L'error que cal evitar és fer servir CRC32 on hi ha un adversari: com a identificador «únic» d'un adjunt, com a comprovació que un fitxer descarregat no ha estat manipulat, o com a part d'un esquema d'autenticació. Un atacant construeix una col·lisió CRC32 en mil·lisegons i en el sentit que vulgui. La regla completa, per tancar: davant d'errors accidentals, CRC32 o SHA-256; davant d'un adversari que modifica dades, HMAC o signatura; davant d'un adversari que roba la base de dades de contrasenyes, Argon2id amb sal única.


Errors Comuns i Consells

Sobre hashos i MAC

  1. Fer servir MD5 o SHA-1 per a seguretat. Estan trencats en col·lisió, i una col·lisió trenca qualsevol signatura construïda al damunt. El mateix val per a CRC32 on hi ha adversari.
  2. Creure que un hash publicat al costat del fitxer protegeix d'un atacant. Només protegeix d'errors: qui pot canviar el fitxer pot canviar el hash.
  3. Construir un MAC com hash(clau || missatge). Vulnerable a extensió de longitud. Fes servir HMAC.
  4. Comparar signatures amb ==. Canal lateral de temporització. Sempre hmac.compare_digest.
  5. Signar el JSON reserialitzat en lloc del cos cru. Produeix fallades intermitents que semblen atacs.
  6. No incloure marca de temps ni comprovar frescor. Sense això, un missatge legítim capturat avui es reenvia d'aquí a un mes.

Sobre contrasenyes

  1. sha256(contrasenya). Deu mil milions d'intents per segon amb una GPU.
  2. Sal fixa, compartida o derivada del correu. Ha de ser aleatòria i única per usuari; deixa que la llibreria la generi.
  3. Implementar Argon2 a mà o copiar un fragment d'un fòrum. Fes servir argon2-cffi o l'Argon2id de cryptography.
  4. Missatges diferents per a «no existeix» i «contrasenya incorrecta», o oblidar el hash esquer. Les dues vies d'enumeració d'usuaris; la segona no es veu llegint el codi.
  5. No posar longitud màxima. Via oberta a denegació de servei.
  6. Fixar els paràmetres i no tornar-los a mirar en cinc anys. Fes servir check_needs_rehash i revisa'ls anualment.

Consells

  • Centralitza el càlcul de hash de contrasenyes en una única funció de la base de codi. Cap altra part del sistema no ha de cridar ph.hash directament.
  • Afegeix una comprovació al CI (02-04) que rebutgi hashlib.md5, hashlib.sha1 i sha256( aplicat a alguna cosa anomenada password, i registra mètriques de temps mitjà de verificació: si puja, hi ha un paràmetre mal calibrat; si baixa de cop, algú ha rebaixat el cost.
  • Recorda la jerarquia: hash per a errors, MAC o signatura per a adversaris, KDF lenta per a contrasenyes. Triar bé entre les tres resol la majoria dels problemes d'aquesta lliçó.

Exercicis

Exercici 1 — Auditar la verificació d'un webhook

Nimbus rep també notificacions del proveïdor d'email transaccional. Aquest és el codi:

import hashlib

SECRET = "nimbus-email-2026"

@app.post("/webhooks/email")
async def rebre(peticio: Request):
    dades = await peticio.json()                                      # (1)
    signatura = peticio.headers.get("X-Signature", "")
    calculada = hashlib.sha256(
        (SECRET + json.dumps(dades)).encode()                         # (2)
    ).hexdigest()
    if calculada != signatura:                                        # (3)
        raise HTTPException(401)
    processar(dades)                                                  # (4)
    return {"ok": True}

Es demana: (a) identifica els quatre problemes marcats i l'atac concret que habilita cadascun; (b) explica per què el problema (2) és una fallada criptogràfica i no només d'estil; (c) reescriu l'endpoint correctament; (d) indica què s'ha de registrar i què no s'ha de registrar en aquest flux.

Exercici 2 — Migrar l'emmagatzematge de contrasenyes

Nimbus té 41.000 credencials desades com a sha256(password) sense sal. La Marta vol migrar a Argon2id sense obligar tothom a canviar la contrasenya de cop.

Es demana: (a) explica per què no es pot convertir directament un hash SHA-256 en un hash Argon2id; (b) dissenya una estratègia de migració progressiva, indicant quina columna afegiries, què passa al següent inici de sessió de cada usuari i què passa amb qui no entra mai; (c) escriu el pseudocodi de la verificació durant la migració; (d) decideix què fer amb els comptes que continuïn en SHA-256 passats sis mesos i justifica-ho; (e) indica si aquesta situació s'ha de tractar com a incident de seguretat i per què.

Exercici 3 — Calibrar i defensar els paràmetres

L'Iván proposa PasswordHasher(memory_cost=8, time_cost=1, parallelism=1) perquè «així el login va instantani».

Es demana: (a) explica què significa cada paràmetre i què implica aquest valor de memòria; (b) estima qualitativament quant s'abarateix la feina de l'atacant davant dels paràmetres recomanats; (c) descriu el procediment que seguiries per calibrar els paràmetres reals al servidor de Nimbus, incloent-hi què mesuraries i amb quina concurrència; (d) formula la resposta que donaries a l'Iván i a la Marta, incloent-hi el compromís entre experiència d'usuari, capacitat del servidor i risc de denegació de servei.


Solucions

Exercici 1

(a) Els quatre problemes:

# Problema Atac
1 Es parseja el JSON i després es reserialitza per signar Qualsevol diferència d'espaiat, ordre de claus o codificació produeix bytes diferents: la signatura legítima falla i es generen falsos rebuigs. I si s'«arregla» relaxant la comparació, s'obre la porta a manipulacions
2 sha256(SECRET + missatge) Extensió de longitud: un atacant que capturi un missatge signat pot afegir-hi dades al final i calcular una signatura vàlida sense conèixer el secret. A més, el secret és al codi
3 Comparació amb != Canal lateral de temporització: es reconstrueix la signatura caràcter a caràcter mesurant temps
4 Sense marca de temps, sense frescor i sense idempotència Repetició: capturar una notificació legítima i reenviar-la demà, tantes vegades com es vulgui

(b) Perquè no és una qüestió de gust sinó de la construcció interna de SHA-2: la seva sortida és el seu estat intern, així que reprendre'l permet continuar el càlcul. L'HMAC no és «el mateix però amb més passos»: la seva segona passada és precisament el que impedeix aquest atac.

(c) Versió correcta. És el patró de l'apartat 6, aplicat a aquest proveïdor:

import hmac, hashlib, time, json

SECRET = obtenir_secret("email/webhook")        # bytes, del gestor (03-06)

@app.post("/webhooks/email")
async def rebre(peticio: Request):
    cos = await peticio.body()                           # bytes CRUS
    marca = peticio.headers.get("X-Timestamp", "")
    signatura = peticio.headers.get("X-Signature", "")
    if not marca or not signatura or abs(time.time() - int(marca)) > 300:
        raise HTTPException(400, "Capcaleres absents o caducades")

    missatge = marca.encode() + b"." + cos
    esperada = hmac.new(SECRET, missatge, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(esperada, signatura):
        registrar_esdeveniment("webhook.email.signatura_invalida", ip=peticio.client.host)
        raise HTTPException(401, "Signatura no valida")

    esdeveniment = json.loads(cos)
    if ja_processat(esdeveniment["id"]):
        return {"ok": True, "estat": "duplicat"}
    processar(esdeveniment)
    return {"ok": True}

(d) Registre. Sí: identificador de l'esdeveniment, tipus, marca de temps, IP d'origen, resultat (acceptat/rebutjat) i motiu del rebuig. No: el cos complet (conté correus i possiblement contingut del missatge), la signatura rebuda completa, el secret, ni cap capçalera d'autenticació. És la regla de 02-04: registrar el suficient per detectar i per investigar, sense convertir el registre en una segona còpia de les dades personals.

Exercici 2

(a) Perquè un hash és unidireccional: de sha256(password) no es pot recuperar la contrasenya, i Argon2id necessita la contrasenya en clar per derivar el seu propi hash. L'única conversió possible seria argon2id(sha256(password)) —calcular el hash del hash—, que funciona però deixa el sistema lligat indefinidament a una capa SHA-256 sense sal, complica la lògica i no és la millor opció si es pot migrar de manera neta.

(b) Estratègia progressiva. Detectar el format pel prefix $argon2id$ de la mateixa cadena (o afegir una columna algorisme). Al següent inici de sessió correcte de cada usuari es disposa de la contrasenya en clar: es verifica amb l'esquema antic i, si és correcta, es recalcula amb Argon2id i es substitueix, sense que l'usuari noti res. Qui no entra mai continua amb el hash antic, i per això cal el pas (d).

(c) Pseudocodi:

def verificar(email, password, repo):
    u = repo.buscar_per_email(email)
    desat = u.hash_password if u else HASH_ESQUER
    if desat.startswith("$argon2id$"):
        ok = argon2_verifica(desat, password)
        if ok and u and ph.check_needs_rehash(desat):
            repo.desar_hash(u.id, ph.hash(password))
    else:
        # Cami heretat: comparacio en temps constant del hash antic
        ok = hmac.compare_digest(
            hashlib.sha256(password.encode()).hexdigest(), desat
        )
        if ok and u:
            repo.desar_hash(u.id, ph.hash(password))    # MIGRACIO
    return bool(ok and u)

(d) Als sis mesos. Els comptes que continuïn en SHA-256 són, gairebé per definició, comptes inactius: un risc doble (hash feble i compte orfe de 02-05). El correcte és forçar el restabliment de contrasenya en aquests comptes —invalidant-los fins que l'usuari completi el flux— i, per als que fa més d'un any que no tenen activitat, avaluar-ne la desactivació d'acord amb la política de cicle de vida. Mantenir indefinidament hashos SHA-256 sense sal és acceptar un risc conegut sense justificació.

(e) És un incident? No és una bretxa —no n'ha sortit res—, però sí que és una troballa de seguretat greu que s'ha de registrar a l'inventari de riscos, amb amo i termini, i tractar-se com a tal (mòdul 4). Si a més existís qualsevol indici que aquella taula s'ha copiat alguna vegada, passaria a ser incident i s'aplicaria l'apartat 12, inclosa la valoració de notificació amb assessoria jurídica.

Exercici 3

(a) memory_cost està en KiB: 8 KiB, és a dir, vuit kilobytes, davant dels 19.456 KiB (19 MiB) recomanats; unes 2.400 vegades menys. time_cost=1 és una sola passada, i parallelism=1 un fil. Amb 8 KiB, Argon2id perd per complet la seva defensa característica: cap folgadament a la memòria cau de cada nucli d'una GPU i esdevé comparable a un hash ràpid.

(b) El cost per intent es redueix en almenys tres ordres de magnitud, i l'avantatge del maquinari paral·lel es restaura per complet. Una cerca que amb paràmetres correctes costaria segles torna a l'ordre de dies o setmanes per a una contrasenya mitjana. Qualitativament: la configuració de l'Iván té el nom d'Argon2id i la seguretat d'un hash ràpid amb sal.

(c) Procediment de calibratge. Mesurar en el maquinari de producció, no al portàtil de desenvolupament; partir de memòria 19 MiB, temps 2, paral·lelisme 1 i cronometrar ph.hash() cent vegades; ajustar fins a situar la mediana entre 250 i 500 ms, pujant primer la memòria; repetir el mesurament amb la concurrència màxima esperada d'inicis de sessió simultanis, vigilant RAM i latència de la resta de l'API; i documentar valors, data, maquinari i percentil 95, amb revisió anual programada.

(d) Resposta a l'Iván i la Marta. L'inici de sessió no necessita ser instantani: 300 ms són imperceptibles per a una persona que acaba de teclejar la seva contrasenya, i passen una vegada per sessió, no a cada petició —la resta del temps l'autenticació la resol el token de 02-05—. Aquest quart de segon és exactament el que multiplica per milions el cost de l'atacant si algun dia es filtra la taula. El compromís real no és contra l'experiència d'usuari, sinó contra la capacitat del servidor: cal dimensionar la RAM per al pic d'inicis simultanis i combinar la mesura amb límit de taxa i bloqueig progressiu (02-05) perquè ningú no pugui fer servir el mateix cost del hash com a vector de denegació de servei.


Conclusió

Has cobert la primitiva que faltava i saldat el deute que el mòdul 2 va deixar pendent. Saps què és una funció hash criptogràfica —sortida fixa, determinista, unidireccional— i les seves tres resistències, amb la de col·lisió com la més fràgil i la primera a caure; has vist l'efecte allau mesurat en bits i per què el 50 % és la xifra correcta. Coneixes l'estat real de la família: MD5 i SHA-1 trencats en col·lisió, amb SHAttered com a referència, i per què una col·lisió destrueix qualsevol signatura construïda al damunt; SHA-256/512, SHA-3 i BLAKE2 com a opcions vigents.

I has après la frontera que separa dos mons: un hash protegeix davant d'accidents; un MAC o una signatura protegeixen davant d'adversaris, perquè qui pot alterar el fitxer també en pot recalcular el sha256sum. D'aquí l'HMAC, amb el perquè exacte que hash(clau || missatge) sigui insegur —l'atac d'extensió de longitud—, aplicat al webhook de la passarel·la de pagament amb les seves quatre decisions deliberades: signar el cos cru, incloure la marca de temps per donar frescor, comparar amb hmac.compare_digest i ser idempotent. I tanques amb la distinció entre sumes de verificació com CRC32 i hashos criptogràfics.

El cor de la lliçó era l'emmagatzematge de contrasenyes, i aquí hi ha la resposta completa que Nimbus necessitava: sha256(contrasenya) és inacceptable perquè és ràpid —deu mil milions d'intents per segon amb una sola GPU—, la sal única per usuari anul·la les taules precalculades i obliga a atacar d'un en un, el pepper viu fora de la base de dades i cobreix el bolcat per injecció SQL, i la solució és una funció de derivació lenta amb cost de memòria: Argon2id com a primera opció, amb 19 MiB, dues passades i un fil com a punt de partida, calibrats fins a 250–500 ms en el maquinari real. Ho has implementat sencer: cadena PHC amb la sal a dins, taula credencials, hash esquer perquè la resposta trigui el mateix existeixi o no l'usuari, i rehash transparent amb check_needs_rehash per endurir paràmetres sense migracions ni molèsties, a més del pla de què fer si la taula es filtra —contenir, invalidar sessions, forçar restabliment i notificar en termini amb validació legal—.

Ja tens les quatre primitives del mòdul: xifratge simètric, xifratge asimètric, hash i MAC, i signatura digital. A Protocols Criptogràfics (03-05) veuràs una cosa incòmoda i necessària: que combinar-les totes correctament no garanteix un sistema segur, perquè l'assemblatge té les seves pròpies fallades. És la lliçó on per fi s'explica en profunditat el TLS que fa vuit lliçons que dones per suposat —la seva encaixada pas a pas, les seves versions, les seves suites, com verificar-lo amb openssl s_client i com configurar-lo bé—, a més de SSH, VPN, mTLS i els atacs clàssics contra protocols: downgrade, stripping, MITM i repetició.

Curs de Fonaments de Seguretat Informàtica

Mòdul 1: Introducció a la Seguretat Informàtica

Mòdul 2: Ciberseguretat

Mòdul 3: Criptografia

Mòdul 4: Gestió de Riscos i Mesures de Protecció

Mòdul 5: Eines i Tècniques de Seguretat

Mòdul 6: Bones Pràctiques i Normatives

Mòdul 7: Projecte Final

© Copyright 2026. Tots els drets reservats