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
- Per què una contrasenya no és una clau
- Què és una KDF i quines propietats té
- KDF per a contrasenyes: PBKDF2, scrypt i Argon2
- El paper del salt
- HKDF: derivar subclaus d'una clau mestra forta
- Cas pràctic: còpia de seguretat exportable de MediNube amb scrypt
- 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:
- 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.
- 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/cryptographyno la porta; es fa servir el paquet a partargon2-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 | Sí | Si necessites FIPS/compatibilitat |
| scrypt | CPU + memòria | Bona | Sí | 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 coincideixDetalls:
Scrypt(...).derive(contrasenya)produeix els 32 bytes. Un objecteScryptés d'un sol ús: per verificar o tornar a derivar, crea'n un de nou.- El
saltde 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çaInvalidKeysi 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:
- 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.
- 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:
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 mestraAmb 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"iinfo=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 quadraObserva 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
v1de 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
decryptde GCM falla ambInvalidTag— 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
-
Puja el cost. Deriva una clau amb scrypt fent servir
n=2**14i cronometra-ho ambtime.perf_counter(); repeteix ambn=2**17. Quant es multiplica el temps? Explica per què aquest encariment perjudica molt més l'atacant que l'usuari legítim. -
La màgia de l'
infoa HKDF. Deriva dues subclaus de la MATEIXAclau_mestraambinfo=b"historials"iinfo=b"copies". Comprova que són diferents. Després deriva una altra vegada ambinfo=b"historials"i comprova que coincideix amb la primera. Quina propietat de les KDF garanteix aquesta reproductibilitat? -
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 ambInvalidTagi 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
-
Passar de
n=2**14an=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. -
Les dues subclaus amb
infodiferent surten diferents; tornar a derivar ambinfo=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 ambinfodiferent, les subclaus queden criptogràficament separades tot i venir de la mateixa mestra. -
Falla amb
InvalidTagperquè 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 idecryptllançaInvalidTag. 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.
Curs de Criptografia Aplicada
Mòdul 1: Fonaments de la Criptografia
- Què és la criptografia i per a què serveix
- Codificació, ofuscació i xifratge
- Aleatorietat i entropia
- El principi de Kerckhoffs i les regles d'or
Mòdul 2: Criptografia Simètrica
- Xifratge simètric: AES i ChaCha20
- Modes d'operació
- Xifratge autenticat (AEAD)
- Derivació de claus (KDF)
Mòdul 3: Hashes, MAC i Contrasenyes
- Funcions hash criptogràfiques
- Autenticació de missatges amb HMAC
- Emmagatzematge segur de contrasenyes
Mòdul 4: Criptografia Asimètrica
- Fonaments de clau pública i RSA
- Criptografia de corba el·líptica
- Signatures digitals
- Intercanvi de claus: Diffie-Hellman
- Xifratge híbrid
Mòdul 5: PKI, Certificats i TLS
- Certificats X.509 i autoritats de certificació
- TLS a la pràctica
- Gestió del cicle de vida dels certificats
