A la lliçó anterior vam tancar el deute del curs: els historials de MediNube ja es xifren en disc amb AES-256-GCM en el format v1. Però cada exemple començava amb un parany que vam prometre resoldre — clau = os.urandom(32) —, un placeholder que esquiva la pregunta: en un sistema real, d'on surt aquesta clau de 32 bytes? L'administrador d'una clínica no memoritza 32 bytes aleatoris; escriu una contrasenya. I una contrasenya no és una clau. Aquesta lliçó cobreix el pont entre tots dos móns: les funcions de derivació de claus (KDF). Aprendràs a convertir contrasenyes en claus amb KDF lentes a propòsit (PBKDF2, scrypt, Argon2), el paper del salt que vam prometre a 01-03, i a derivar múltiples subclaus d'una clau mestra forta amb HKDF. Com a última lliçó del mòdul, tancarem el mòdul sencer i n'obrirem el següent.

Contingut

  1. Per què una contrasenya no és una clau
  2. Què és una KDF i quines propietats té
  3. KDF per a contrasenyes: PBKDF2, scrypt i Argon2
  4. El paper del salt
  5. HKDF: derivar subclaus d'una clau mestra forta
  6. Cas pràctic: còpia de seguretat exportable de MediNube amb scrypt
  7. Derivar claus per xifrar vs desar contrasenyes per autenticar

Per què una contrasenya no és una clau

Una clau AES-256 són 32 bytes d'entropia pura (256 bits reals), sortits del CSPRNG (01-03). Una contrasenya humana és una cosa molt diferent:

Clau criptogràfica Contrasenya humana
Longitud Fixa (32 bytes) Variable (6, 12, 40 caràcters...)
Composició Bytes qualssevol (0–255) Caràcters imprimibles, patrons, paraules
Entropia Màxima: 256 bits reals Baixa: "Estiu2026!" té potser 30-40 bits
Origen CSPRNG La memòria d'una persona

Dos problemes t'impedeixen usar una contrasenya directament com a clau:

  1. Format incompatible. AES-256 exigeix exactament 32 bytes. Una contrasenya d'11 caràcters són 11 bytes; una de 40, quaranta. No hi encaixa.
  2. Entropia insuficient i estructurada. Encara que la retalléssis o l'omplíssis fins a 32 bytes, continuaria tenint la baixa entropia d'origen: un atacant no provaria 2^256 claus, provaria el diccionari de contrasenyes comunes — milions, no quatrilions. La fortalesa teòrica d'AES-256 s'esfondra fins a la de la contrasenya.

Necessitem una funció que agafi aquesta contrasenya feble i de longitud variable i produeixi una clau de 32 bytes amb el format correcte i que, a més, faci l'atac per diccionari tan car com sigui possible. Això és una KDF.

Què és una KDF i quines propietats té

Una KDF (Key Derivation Function, funció de derivació de claus) transforma un material d'entrada (una contrasenya, o una clau mestra) en una o més claus criptogràfiques amb la mida i les propietats desitjades. Propietats que defineixen una bona KDF:

  • Sortida de longitud fixa i uniforme. Produeix exactament els bytes que demanis (32 per a AES-256), i aquests bytes són indistingibles d'aleatoris encara que l'entrada tingui estructura.
  • Determinista. La mateixa entrada (mateixa contrasenya + mateix salt + mateixos paràmetres) produeix sempre la mateixa clau. Imprescindible: si no, no podries desxifrar demà el que xifrem avui.
  • Unidireccional. De la clau de sortida no es pot tornar a la contrasenya.
  • Costosa a voluntat (per a les KDF de contrasenyes): parametritzable per trigar deliberadament, encarint la força bruta.

Aquí hi ha una divisió fonamental que estructura la resta de la lliçó:

  • KDF lentes per a entrades de baixa entropia (contrasenyes): PBKDF2, scrypt, Argon2. La seva gràcia és ser cares — com més costa cada intent, menys intents per segon pot fer un atacant.
  • KDF ràpides per a entrades ja fortes (una clau mestra de 256 bits): HKDF. Aquí no cal alentir res, perquè l'entrada ja té tota l'entropia; només volem repartir-la en diverses subclaus.

Usar l'eina equivocada és un error clàssic: HKDF sobre una contrasenya no la protegeix (és ràpida, la força bruta vola); una KDF lenta per trossejar una clau mestra només malgasta CPU.

KDF per a contrasenyes: PBKDF2, scrypt i Argon2

Totes tres deriven una clau d'una contrasenya, i totes tres són lentes a propòsit. Es diferencien en com imposen el cost, cosa que determina la seva resistència a diferents atacants (que avui usen GPU i maquinari dedicat, no CPU).

  • PBKDF2: la clàssica (i la que exigeix FIPS). El seu cost és el nombre d'iteracions: repeteix un HMAC desenes o centenars de milers de vegades. Problema: només costa còmput, i les GPU fan còmput en massa molt barat. Continua sent acceptable amb moltes iteracions, però és la més feble de les tres davant maquinari modern.
  • scrypt: afegeix cost de memòria (memory-hard). Obliga a usar una gran quantitat de RAM a més de CPU, i la memòria és cara de paral·lelitzar en GPU/ASIC. Molt millor que PBKDF2 contra atacants amb maquinari especialitzat. Ve inclosa a pyca/cryptography.
  • Argon2: l'estat de l'art, guanyador de la Password Hashing Competition (2015). Memory-hard i amb paràmetres independents de temps, memòria i paral·lelisme. La variant recomanada és Argon2id. pyca/cryptography no la porta; es fa servir el paquet a part argon2-cffi (pip install argon2-cffi).
KDF Cost que imposa Resistència a GPU/ASIC A pyca/cryptography Quan triar-la
PBKDF2 Iteracions (només CPU) Baixa-mitjana Si necessites FIPS/compatibilitat
scrypt CPU + memòria Bona Bona opció general amb el que porta la llibreria
Argon2id CPU + memòria + paral·lelisme ajustables La millor No (usa argon2-cffi) Recomanada per a projectes nous

Paràmetres recomanats orientatius (2026; verifica'ls sempre amb les guies OWASP vigents, perquè pugen amb el maquinari):

  • PBKDF2-HMAC-SHA256: ≥ 600.000 iteracions.
  • scrypt: n = 2^17 (131072), r = 8, p = 1 (≈128 MB de memòria).
  • Argon2id: memòria ≥ 19 MiB, ≥ 2 iteracions, paral·lelisme 1 (mínims OWASP; puja la memòria si el servidor ho permet).

La regla per calibrar: ajusta els paràmetres perquè derivar UNA clau trigui entre 0,1 i 0,5 s al teu maquinari. És un parpelleig per al teu usuari legítim (deriva una vegada), però multiplica per milions el cost de l'atacant que prova diccionaris. Exemple amb scrypt, la que farem servir a MediNube:

import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt

contrasenya = "C0ntrasenya-admin-Sol!".encode("utf-8")   # baixa entropia, longitud variable
salt = os.urandom(16)                                    # 16 bytes del CSPRNG, ÚNIC (vegeu més avall)

def deriva_clau(contrasenya: bytes, salt: bytes) -> bytes:
    kdf = Scrypt(
        salt=salt,
        length=32,        # volem 32 bytes -> clau AES-256
        n=2**17,          # cost de CPU/memòria (potència de 2)
        r=8,
        p=1,
    )
    return kdf.derive(contrasenya)

clau = deriva_clau(contrasenya, salt)
print(len(clau), clau.hex())    # 32 bytes d'aspecte aleatori

# Verificar una contrasenya candidata: es RE-deriva amb el MATEIX salt i es compara.
kdf_v = Scrypt(salt=salt, length=32, n=2**17, r=8, p=1)
kdf_v.verify(contrasenya, clau)   # no llança -> correcta; InvalidKey si no coincideix

Detalls:

  • Scrypt(...).derive(contrasenya) produeix els 32 bytes. Un objecte Scrypt és d'un sol ús: per verificar o tornar a derivar, crea'n un de nou.
  • El salt de 16 bytes s'ha de desar (ho veiem ara): sense ell no es pot re-derivar la mateixa clau.
  • kdf.verify(contrasenya, clau_esperada) re-deriva i compara en temps constant; llança InvalidKey si no quadra.

El paper del salt

El salt és un valor aleatori i únic que es combina amb la contrasenya abans de derivar. A 01-03 vam prometre explicar els salts; aquí és la seva raó de ser. Sense salt, deriva(contrasenya) seria una funció fixa, i això obre dos atacs:

  1. Taules precalculades (rainbow tables). Un atacant precalcula la derivació de milions de contrasenyes comunes una vegada i després busca coincidències instantàniament. El salt el mata: amb un salt diferent per usuari, la taula s'hauria de refer per a cada salt — inviable.
  2. Atacs en lot. Sense salt, dos administradors amb la mateixa contrasenya produeixen la mateixa clau, i trencar-ne una les trenca totes dues. Un atacant ataca tothom alhora. Amb salt únic, cada derivació és un problema independent: cal atacar d'un en un.

Propietats del salt, que sorprenen a qui comença:

  • És únic per derivació (os.urandom(16) cada vegada).
  • És aleatori, del CSPRNG.
  • NO és secret. Es desa en clar, junt amb el resultat. La seva feina no és amagar-se, sinó ser diferent en cada cas per individualitzar cada derivació. És exactament la mateixa filosofia que l'IV/nonce de 02-02: públic però irrepetible.

Per això, en disc, un registre protegit amb contrasenya sempre desa el salt al costat:

desat = salt (16 bytes, en clar)  ||  dades derivades/xifrades

HKDF: derivar subclaus d'una clau mestra forta

Canviem d'escenari. De vegades l'entrada ja és forta: una clau mestra de 256 bits d'entropia real (la de MediNube). No volem "protegir-la" amb lentitud — ja és inatacable per força bruta —; volem derivar-ne diverses subclaus independents, una per propòsit. Per què no usar la mateixa clau mestra per a tot? Per separació de dominis: si cada ús té la seva subclau, comprometre o reutilitzar-ne una no afecta les altres, i evites col·lisions de nonce entre subsistemes diferents.

L'eina és HKDF (HMAC-based KDF), ràpida perquè l'entrada ja té entropia. Treballa en dues fases conceptuals: extract (condensa l'entrada en una clau interna uniforme) i expand (l'estira en tants bytes com demanis). El seu paràmetre estrella és info: una etiqueta de context que separa les derivacions. Mateixa clau mestra + info diferent = subclaus criptogràficament independents.

import os
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

clau_mestra = os.urandom(32)    # entrada JA forta (256 bits reals)

def subclau(mestra: bytes, context: bytes) -> bytes:
    hkdf = HKDF(
        algorithm=hashes.SHA256(),
        length=32,                 # 32 bytes -> subclau AES-256
        salt=None,                 # opcional aquí; l'entrada ja és de màxima entropia
        info=context,              # ETIQUETA que separa aquesta subclau de les altres
    )
    return hkdf.derive(mestra)

k_historials = subclau(clau_mestra, b"medinube:historiales:v1")
k_copies_seguretat = subclau(clau_mestra, b"medinube:backups:v1")

print(k_historials != k_copies_seguretat)   # True: dues subclaus independents d'UNA mestra

Amb això, la clau mestra de MediNube dona lloc a k_historials (la que alimenta el xifrar_historial_v1 de 02-03) i a k_copies_seguretat (la dels respatlles), sense que cap reveli res de l'altra. Nota la diferència amb les KDF de contrasenya: aquí info reemplaça el protagonisme del salt, no hi ha paràmetre de cost (és ràpida a propòsit), i l'entrada ha de ser forta de partida — HKDF no rescata una contrasenya feble.

Nota tècnica: les etiquetes info=b"medinube:historiales:v1" i info=b"medinube:backups:v1" són identificadors de protocol interns de MediNube i es mantenen sense traduir, igual que la resta d'aquest tipus de cadenes al llarg del curs.

Cas pràctic: còpia de seguretat exportable de MediNube amb scrypt

Ajuntem les peces del mòdul en un cas real. MediNube permet a un administrador de clínica exportar una còpia de seguretat xifrada dels seus historials, protegida per una passphrase que només ell coneix (per poder-la restaurar en un altre entorn sense dependre de la clau mestra del servidor). El flux uneix scrypt (contrasenya → clau) amb AES-256-GCM (02-03):

import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

SCRYPT_N, SCRYPT_R, SCRYPT_P = 2**17, 8, 1

def exportar_copia_seguretat(passphrase: str, dades: bytes) -> bytes:
    """salt(16) || nonce(12) || AES-256-GCM(xifrat+tag). La passphrase NO es desa."""
    salt = os.urandom(16)
    clau = Scrypt(salt=salt, length=32, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P).derive(
        passphrase.encode("utf-8"))
    nonce = os.urandom(12)
    aad = b"medinube-backup;formato=v1"
    xifrat = AESGCM(clau).encrypt(nonce, dades, aad)
    return salt + nonce + xifrat

def restaurar_copia_seguretat(passphrase: str, blob: bytes) -> bytes:
    """Re-deriva la clau amb el salt desat i desxifra. Falla si la passphrase és incorrecta."""
    salt, nonce, xifrat = blob[:16], blob[16:28], blob[28:]
    clau = Scrypt(salt=salt, length=32, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P).derive(
        passphrase.encode("utf-8"))
    aad = b"medinube-backup;formato=v1"
    return AESGCM(clau).decrypt(nonce, xifrat, aad)   # InvalidTag si la passphrase és incorrecta

# Ús:
dades = "Copia de seguretat Clinica Sol: historial d'Ana Perez, ...".encode("utf-8")
blob = exportar_copia_seguretat("Frase-llarga-i-secreta-de-ladmin-2026", dades)

print(restaurar_copia_seguretat("Frase-llarga-i-secreta-de-ladmin-2026", blob).decode("utf-8"))  # OK

try:
    restaurar_copia_seguretat("passphrase-incorrecta", blob)
except Exception as e:
    print("Rebutjat:", type(e).__name__)    # InvalidTag: la clau derivada no quadra

Observa com encaixa tot el mòdul:

  • El salt (16 bytes, públic) es desa al principi del blob: sense ell no es pot re-derivar la clau. El nonce (12 bytes, públic, únic) també viatja inclòs — com al format v1 de 02-03.
  • La passphrase mai s'emmagatzema: només existeix al cap de l'administrador. Si s'equivoca en restaurar, la clau derivada és una altra i el decrypt de GCM falla amb InvalidTag — la integritat d'AEAD detecta la passphrase incorrecta "gratis".
  • scrypt fa que un atacant que robi el blob i vulgui provar passphrases per diccionari pagui el cost de scrypt a cada intent.

Dades fictícies, com sempre; un desplegament real de còpies de seguretat sanitàries exigeix revisió de seguretat i compliance (RGPD).

Derivar claus per xifrar vs desar contrasenyes per autenticar

Una distinció crucial que evita un dels embolics més habituals. En aquesta lliçó hem usat KDF amb contrasenyes per derivar una clau i xifrar dades (còpies de seguretat, historials). Existeix un ús semblant però diferent de les mateixes famílies de funcions: emmagatzemar contrasenyes d'usuari per autenticar-los quan inicien sessió (el clàssic "iniciar sessió d'ana.perez al portal").

Derivar clau per xifrar (aquesta lliçó) Desar contrasenya per autenticar (03-03)
Objectiu Obtenir una clau per a AES Verificar que qui entra sap la contrasenya
Què es desa La dada xifrada (+ salt, nonce) El hash de la contrasenya (+ salt, paràmetres)
La clau derivada... s'usa per xifrar/desxifrar no s'usa per a res; només es compara
Funcions scrypt/Argon2/PBKDF2 (+ HKDF per a subclaus) scrypt/Argon2/PBKDF2, en mode "hash de contrasenya"

Comparteixen la maquinària (KDF lentes, salt, cost), però el propòsit difereix: aquí la clau derivada és un mitjà per xifrar; en autenticació, la sortida és la finalitat — es desa per recomprovar-la a cada login, i mai es xifra res amb ella. Aquest segon escenari, l'emmagatzematge segur de contrasenyes d'usuari, es desenvolupa per complet a la lliçó 03-03. Aquí només ho anunciem perquè no confonguis els dos usos.

Errors Comuns i Consells

  • Usar una contrasenya directament com a clau AES. L'error que obre la lliçó. Sempre passa per una KDF de contrasenya.
  • Usar una KDF ràpida (HKDF, o un hash simple) sobre una contrasenya. No la protegeix: la força bruta vola. Per a contrasenyes, KDF lenta (scrypt/Argon2/PBKDF2). HKDF és només per a entrades ja fortes.
  • Reutilitzar el salt, o creure'l secret. El salt és únic per derivació i públic; es desa en clar junt amb el resultat. Reutilitzar-lo reobre les rainbow tables i els atacs en lot.
  • Copiar paràmetres de cost d'un tutorial vell. Pugen amb el maquinari: 1.000 iteracions de PBKDF2 va ser segur el 2010 i avui és una broma. Calibra a 0,1–0,5 s i consulta OWASP.
  • Oblidar desar salt/nonce/paràmetres. Sense ells no es pot re-derivar ni desxifrar. Desa'ls junt amb la dada (ho fa el blob de la còpia de seguretat).
  • Confondre derivar-per-xifrar amb desar-per-autenticar. Finalitat diferent; l'autenticació de contrasenyes és 03-03.

Exercicis

  1. Puja el cost. Deriva una clau amb scrypt fent servir n=2**14 i cronometra-ho amb time.perf_counter(); repeteix amb n=2**17. Quant es multiplica el temps? Explica per què aquest encariment perjudica molt més l'atacant que l'usuari legítim.

  2. La màgia de l'info a HKDF. Deriva dues subclaus de la MATEIXA clau_mestra amb info=b"historials" i info=b"copies". Comprova que són diferents. Després deriva una altra vegada amb info=b"historials" i comprova que coincideix amb la primera. Quina propietat de les KDF garanteix aquesta reproductibilitat?

  3. Passphrase incorrecta detectada gratis. Amb exportar_copia_seguretat/restaurar_copia_seguretat, exporta una còpia de seguretat i restaura-la amb una passphrase equivocada. Per què falla amb InvalidTag i no amb un error de "clau incorrecta" explícit? Quins dos mecanismes del mòdul (scrypt + AES-GCM) col·laboren per produir aquesta fallada?

Solucions

  1. Passar de n=2**14 a n=2**17 és multiplicar el cost per 8, així que el temps es multiplica aproximadament per 8 (p. ex. de ~15 ms a ~120 ms). Per a l'usuari legítim és un parpelleig: deriva una sola vegada en exportar o restaurar. Per a l'atacant que prova un diccionari de, diguem-ne, mil milions de passphrases, aquest factor 8 multiplica per 8 el cost de cadascun d'aquests mil milions d'intents — de dies a setmanes. L'asimetria (tu en derives una, l'atacant en deriva milions) és exactament el que fa útil la lentitud.

  2. Les dues subclaus amb info diferent surten diferents; tornar a derivar amb info=b"historials" reprodueix exactament la primera subclau. Ho garanteix la propietat determinista de les KDF: mateixa entrada (clau mestra + info + paràmetres) → mateixa sortida. Sense ella no podríem tornar a obtenir la subclau per desxifrar demà el que hem xifrat avui. I amb info diferent, les subclaus queden criptogràficament separades tot i venir de la mateixa mestra.

  3. Falla amb InvalidTag perquè una passphrase equivocada, passada per scrypt amb el salt desat, produeix una clau derivada diferent de l'original. En intentar desxifrar amb aquesta clau errònia, el tag d'AES-GCM (02-03) no valida i decrypt llança InvalidTag. Hi col·laboren els dos mecanismes: scrypt converteix "passphrase incorrecta" en "clau incorrecta" de forma determinista, i AES-GCM converteix "clau incorrecta" en "fallada detectada" gràcies a la seva verificació d'integritat. No cal comprovar la passphrase per separat: la pròpia AEAD ho fa, i en temps constant.

Conclusió

Amb la derivació de claus tanquem el Mòdul 2: Criptografia Simètrica i, amb ell, l'arc que va començar al mòdul 1. El recorregut ha estat complet: hem conegut AES i ChaCha20 (02-01), hem après que un xifratge en bloc necessita un mode i per què ECB filtra mentre CBC i CTR exigeixen un IV/nonce irrepetible (02-02), hem fet el salt al xifratge autenticat que uneix confidencialitat i integritat i hem saldat per fi el deute del curs xifrant els historials de MediNube amb AES-256-GCM en el format v1 (02-03), i en aquesta lliçó hem resolt l'última incògnita — d'on surten les claus: convertir contrasenyes en claus amb KDF lentes (PBKDF2, scrypt, Argon2) protegides per un salt únic i públic, i repartir una clau mestra forta en subclaus independents amb HKDF. El balanç de MediNube ha canviat completament des del final del mòdul 1: els historials d'Ana Pérez ja no estan sense xifrar en disc, sinó protegits amb xifratge autenticat; existeix un mòdul centralitzat i criptoàgil medinube.cripto; i sabem derivar tant la clau mestra des d'una passphrase d'administrador com les subclaus per propòsit. La base simètrica està construïda. El que encara ens falta són les peces que garanteixen la integritat i la identitat sense xifrar res: com es comprova que un fitxer no ha canviat sense desxifrar-lo? Com s'autentica un missatge entre dues parts que comparteixen una clau? I la pregunta que hem fregat dues vegades — com es desen de forma segura les contrasenyes dels usuaris que inicien sessió al portal de MediNube, un problema diferent al de derivar claus per xifrar? Tot això és el Mòdul 3: Hashes, MAC i Contrasenyes, que comença a la lliçó 03-01 amb les funcions hash criptogràfiques — la primitiva sobre la qual es construeixen HMAC (03-02) i l'emmagatzematge segur de contrasenyes (03-03). Ens veiem al mòdul 3.

© Copyright 2026. Tots els drets reservats