Tota la criptografia descansa sobre un únic fonament: valors que un atacant no pot predir. Les claus de xifratge, els tokens de sessió, les sals que acompanyaran les contrasenyes (mòdul 3) i els IV que acompanyaran el xifratge (mòdul 2) — tots són, en el fons, "números aleatoris". Si l'aleatorietat falla, falla tot el que hi ha construït a sobre, per perfecte que sigui l'algorisme: és l'equivalent criptogràfic de construir un búnquer amb la porta oberta. En aquesta lliçó entendràs què és l'entropia, la diferència crítica entre generadors pseudoaleatoris normals (PRNG) i criptogràfics (CSPRNG), d'on treu el sistema operatiu la seva aleatorietat, i com generar valors segurs en Python amb secrets i os.urandom — incloent-hi el token de recuperació de contrasenya per als usuaris de MediNube.

Contingut

  1. Per què l'aleatorietat és la base de tot
  2. Entropia: mesurar la imprevisibilitat
  3. PRNG vs CSPRNG
  4. D'on treu l'entropia el sistema operatiu
  5. En Python: random (NO) vs secrets i os.urandom (SÍ)
  6. Cas MediNube: token de recuperació de contrasenya
  7. Espai de claus i bits de seguretat: per què 2^128 és inabastable

Per què l'aleatorietat és la base de tot

Recorda la lliçó anterior: la seguretat del xifratge no rau en el secret de l'algorisme sinó en la clau. Això només funciona si la clau és imprevisible. Repassem quins valors imprevisibles necessita un sistema com MediNube:

Valor Per a què serveix Què passa si és predictible
Claus de xifratge Xifrar historials i documents L'atacant regenera la clau i desxifra tot
Tokens de sessió Identificar el metge que ha iniciat sessió Segrest de sessions alienes
Tokens de recuperació de contrasenya Enllaç de "he oblidat la contrasenya" Prendre el control de qualsevol compte
Sals (mòdul 3) Acompanyar el hash de cada contrasenya Atacs precalculats contra la taula de contrasenyes
IV / nonces (mòdul 2) Que xifrar dues vegades el mateix no es noti Fuites de patrons en el text xifrat
Identificadors no endevinables URL de documents compartits Enumeració de recursos aliens

I no és un risc teòric. Dos exemples històrics famosos:

  • Netscape (1995): el generador de claus de sessió SSL s'alimentava de l'hora del dia i l'ID del procés. Un atacant que conegués aproximadament quan es va crear la connexió reduïa les possibilitats a un grapat i trencava la sessió en segons.
  • Debian OpenSSL (2006-2008): un pedaç va eliminar per error gairebé tota la font d'entropia del generador; durant gairebé dos anys, les claus SSH i SSL generades a Debian sortien d'un espai de només 32.767 possibilitats. Es van poder regenerar totes i trencar totes.

En tots dos casos els algorismes de xifratge eren correctes. Va fallar el fonament.

Entropia: mesurar la imprevisibilitat

L'entropia (en el sentit de la teoria de la informació) mesura quanta incertesa té un valor per a qui intenta endevinar-lo, i s'expressa en bits. La intuïció:

  • Un valor amb n bits d'entropia és tan difícil d'endevinar com una seqüència de n llançaments d'una moneda perfecta: hi ha 2^n possibilitats igual de probables.
  • 1 bit = 2 possibilitats. 10 bits = 1.024. 20 bits ≈ un milió. 128 bits ≈ 3,4 × 10^38.

El crucial és que l'entropia mesura les possibilitats des del punt de vista de l'atacant, no l'aspecte del valor:

  • 7f3k9x2m sembla aleatori, però si es va generar triant entre 1.000 valors possibles (per exemple, a partir d'una marca de temps en segons de l'última hora), té ~10 bits d'entropia: es trenca a l'instant.
  • La longitud enganya: un UUID versió 1 és llarg, però es construeix amb l'hora i l'adreça MAC — molta longitud, poca entropia. (Els UUID versió 4 sí que són aleatoris, amb 122 bits.)
  • Una contrasenya de 20 caràcters formada per dues paraules del diccionari i un any (clinicasol2026metge) té molta menys entropia que 10 caràcters uniformement aleatoris.

Regla mental: entropia = log2(nombre de valors possibles equiprobables). Quan dubtis d'un generador, no preguntis "quin aspecte té la sortida?" sinó "quants valors diferents va poder produir i els podia l'atacant estrènyer?".

PRNG vs CSPRNG

Els ordinadors són màquines deterministes: no produeixen atzar del no-res. El que fan és expandir una llavor mitjançant un algorisme. I aquí es bifurquen dos mons:

PRNG (Pseudo-Random Number Generator)

Dissenyat per a estadística i simulació: que la sortida "sembli" uniforme, sigui rapidíssima i (sovint) reproduïble a voluntat. El mòdul random de Python fa servir Mersenne Twister, excel·lent per simular daus o barrejar dades de prova... i catastròfic per a la seguretat:

  • És determinista i recuperable: observant 624 sortides de 32 bits, un atacant reconstrueix l'estat intern complet i prediu totes les sortides futures (hi ha eines públiques que ho fan).
  • Es pot sembrar explícitament (random.seed(42)), i per defecte es pot sembrar amb fonts pobres. La reproduïbilitat, virtut en simulació, és verí en seguretat.

CSPRNG (Cryptographically Secure PRNG)

Dissenyat per a adversaris: encara que l'atacant conegui l'algorisme (Kerckhoffs, lliçó 01-04) i observi qualsevol quantitat de sortides, no pot predir la següent ni reconstruir les anteriors. Es sembra contínuament amb entropia real del sistema i el seu estat intern està protegit.

Propietat PRNG (random) CSPRNG (secrets, os.urandom)
Objectiu de disseny Bona distribució estadística, velocitat Imprevisibilitat davant d'un adversari
Predictible observant sortides? (Mersenne Twister: amb 624 sortides) No (computacionalment inviable)
Sembrable/reproduïble? Sí (seed()) No des de l'aplicació
Font de la llavor Pot ser pobra Entropia del sistema operatiu
Usos correctes Simulacions, jocs, tests, dades de prova Claus, tokens, sals, IV, tot el de seguretat
En Python random.* secrets.*, os.urandom()

La regla és binària i sense excepcions: si un atacant guanya alguna cosa endevinant el valor, CSPRNG. Sempre.

D'on treu l'entropia el sistema operatiu

Si l'ordinador és determinista, d'on surt la imprevisibilitat inicial? El sistema operatiu recull soroll físic del món real: microvariacions de temps entre interrupcions de maquinari, moviments de ratolí i pulsacions de teclat, temps d'accés a disc, soroll tèrmic, i en CPU modernes, instruccions de maquinari dedicades (com RDRAND en x86). Totes aquestes fonts es barregen en un "pool" d'entropia que alimenta un CSPRNG del nucli.

A Linux, aquest CSPRNG del nucli s'exposa de tres maneres:

  • /dev/urandom: dispositiu clàssic; retorna bytes del CSPRNG sense bloquejar-se. És la interfície correcta per a pràcticament tot.
  • /dev/random: històricament es bloquejava quan "s'esgotava" un comptador d'entropia — una precaució avui considerada innecessària (un cop ben sembrat, un CSPRNG no "es gasta"). En nuclis moderns (≥ 5.6) es comporta gairebé igual que urandom.
  • getrandom(): la crida al sistema moderna (des de Linux 3.17). Com urandom, però amb una garantia extra important: només es bloqueja si el pool encara no s'ha inicialitzat (per exemple, en els primers instants de l'arrencada d'una màquina virtual acabada de clonar, l'únic escenari on urandom podia ser perillós). És el que fa servir Python per sota quan hi és disponible.
graph TD
    F1[Temps d'interrupcions] --> P[Pool d'entropia del nucli]
    F2[Ratolí / teclat / disc] --> P
    F3[RNG de maquinari<br/>RDRAND, TPM] --> P
    P --> CS[CSPRNG del nucli]
    CS --> U["/dev/urandom"]
    CS --> G["getrandom()"]
    G --> PY["Python: os.urandom / secrets"]
    U --> PY

Per a tu com a desenvolupador la conclusió és tranquil·litzadora: no gestionis l'entropia a mà. No barregis fonts pròpies, no "afegeixis aleatorietat" amb marques de temps. Demana bytes al sistema operatiu a través de les API correctes i prou. Windows i macOS tenen mecanismes equivalents (CryptGenRandom/BCryptGenRandom, getentropy), i Python els abstreu tots.

En Python: random (NO) vs secrets i os.urandom (SÍ)

El mòdul equivocat per a seguretat: random

import random

random.seed(1234)                 # reproduïble: útil en tests, fatal en seguretat!
print(random.randint(0, 999999))  # 997185 — sempre el mateix amb aquesta llavor
print(random.random())            # 0.9664535356921388 — ídem

La mateixa documentació de Python ho adverteix: "els generadors pseudoaleatoris d'aquest mòdul no s'han d'usar amb finalitats de seguretat". Tot el que surti de random s'ha de considerar conegut per l'atacant.

Els mòduls correctes: os.urandom i secrets

os.urandom(n) retorna n bytes del CSPRNG del sistema (via getrandom() en Linux modern). secrets (des de Python 3.6) és una capa de conveniència sobre la mateixa font, amb funcions pensades exactament per als casos de seguretat:

import os
import secrets

# Bytes crus: la forma de generar material per a una futura clau
material_clau = os.urandom(32)          # 32 bytes = 256 bits
print(material_clau.hex())              # p. ex. '9f86d081884c7d659a2f...'

# Equivalent amb secrets:
material_clau = secrets.token_bytes(32)

# Bytes representats en hexadecimal (str a punt per desar/mostrar)
print(secrets.token_hex(16))             # 16 bytes -> 32 caràcters hex

# Bytes en Base64 URL-safe (str a punt per ficar en una URL)
print(secrets.token_urlsafe(32))         # p. ex. 'Drmhze6EPcv0fN_81Bj-nA...'

# Eleccions segures sobre col·leccions
alfabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"   # sense caràcters ambigus (0/O, 1/I)
codi_invitacio = "".join(secrets.choice(alfabet) for _ in range(10))

# Enters segurs
dau_segur = secrets.randbelow(6) + 1   # enter uniforme a [1, 6]

Punt per punt:

  • os.urandom(32) i secrets.token_bytes(32) són equivalents; fes servir el que faci el codi més llegible. 32 bytes = 256 bits d'entropia, la mida que farem servir per a claus AES-256 al mòdul 2.
  • token_hex(n) i token_urlsafe(n) reben el nombre de bytes d'entropia i retornen un str ja codificat (lliçó 01-02 en acció: el token són bytes, l'hex/Base64 és el seu embalatge!). token_urlsafe(32) produeix ~43 caràcters.
  • secrets.choice és la versió segura de random.choice; secrets.randbelow(n) dona enters uniformes sense biaix (evita el clàssic error de os.urandom(1)[0] % 6, que introdueix biaix perquè 256 no és múltiple de 6).
  • Quant demanar? Per a tokens, 32 bytes (256 bits) és una elecció generosa i estàndard; 16 bytes (128 bits) és el mínim seriós avui.

Cas MediNube: token de recuperació de contrasenya

El flux "he oblidat la contrasenya" de MediNube envia per correu un enllaç amb un token. Si aquest token és endevinable, un atacant pren el control del compte de qualsevol metge o pacient sense tocar la contrasenya. Compara les dues implementacions:

# ─── VERSIÓ VULNERABLE (trobada en el codi heretat de MediNube) ───
import random, time

def token_recuperacio_MALAMENT(usuari: str) -> str:
    random.seed(time.time())                    # llavor = hora actual
    return f"{usuari}-{random.randint(100000, 999999)}"
    # p. ex. 'ana.perez-483920'

Problemes, de més a menys greu:

  1. Llavor predictible: l'atacant sap (per la capçalera del correu, o simplement provant) en quin segon es va demanar el token; provant unes desenes de llavors properes regenera el token exacte.
  2. Espai minúscul: encara que la llavor fos perfecta, hi ha 900.000 valors — ~20 bits d'entropia. Un script els prova tots contra l'endpoint en minuts.
  3. PRNG no criptogràfic: random és predictible per disseny, com ja sabem.
  4. Inclou el nom d'usuari, regalant estructura.
# ─── VERSIÓ CORRECTA ───
import secrets

def generar_token_recuperacio() -> str:
    """Token d'un sol ús per restablir la contrasenya. 256 bits d'entropia."""
    return secrets.token_urlsafe(32)

token = generar_token_recuperacio()
enllac = f"https://portal.medinube.example/recuperar?token={token}"
# https://portal.medinube.example/recuperar?token=hklR3ZbG8vN2wq...
  • secrets.token_urlsafe(32): 32 bytes del CSPRNG del sistema → 2^256 possibilitats. Impossible d'endevinar, impossible de regenerar, sense estructura.
  • El format URL-safe permet posar-lo directament a l'enllaç del correu (lliçó 01-02).
  • La criptografia resol la imprevisibilitat; la resta és lògica d'aplicació que també importa: caducitat curta (p. ex. 30 minuts), un sol ús, invalidar-lo en usar-se. I com veurem al mòdul 3, a la base de dades convé desar un hash del token, no el token mateix — igual que amb les contrasenyes.

Advertiment: en un sistema sanitari real, el flux complet de recuperació de comptes (correu, caducitats, auditoria) s'ha de revisar amb professionals de seguretat i compliment (RGPD); aquí només mostrem la peça criptogràfica.

Espai de claus i bits de seguretat: per què 2^128 és inabastable

Parlem de "n bits de seguretat" quan el millor atac conegut exigeix de l'ordre de 2^n operacions. Per a la força bruta pura, n és la mida de l'espai de claus. La progressió és brutal, perquè cada bit duplica el treball:

Bits Valors possibles Força bruta viable?
20 ~10^6 Instantània en un portàtil
40 ~10^12 Minuts o hores en un portàtil
56 (DES, 1977) ~7 × 10^16 Trencat en dies ja el 1998 (màquina de 250.000 $); avui trivial
80 ~10^24 Al límit d'actors estatals; ja no es considera segura
128 ~3,4 × 10^38 No, ni de bon tros, per a ningú
256 ~10^77 Marge extra (rellevant davant la computació quàntica, mòdul 6)

Per què 2^128 és una barrera física i no només pràctica? Fem el càlcul de forma conservadora i generosa amb l'atacant:

intents_per_segon = 10**12         # un bilió de claus/s per màquina (molt generós)
maquines = 10**9                   # mil milions de màquines en paral·lel
segons_per_any = 3.15 * 10**7

anys = 2**128 / (intents_per_segon * maquines * segons_per_any)
print(f"{anys:.1e} anys")          # ~1.1e+10 anys

Amb mil milions de màquines provant un bilió de claus per segon cadascuna, esgotar 2^128 claus portaria de l'ordre de 10.800 milions d'anys — comparable a l'edat de l'univers (~13.800 milions). I l'energia necessària per simplement comptar fins a 2^128 amb l'eficiència física teòrica màxima excedeix tota la producció energètica de la humanitat per marges absurds. Per això els atacs reals mai són força bruta contra una bona clau: ataquen claus mal generades (aquesta lliçó), algorismes que filtren patrons (mòdul 2), contrasenyes humanes de baixa entropia (mòdul 3) o implementacions defectuoses (lliçó 01-04).

Compte amb la lletra petita: 128 bits de seguretat requereixen 128 bits d'entropia real. Una clau de 256 bits derivada d'una contrasenya de 30 bits d'entropia és una clau de 30 bits disfressada — el pont entre contrasenyes i claus (KDF) es construeix al mòdul 2 (lliçó 02-04).

Dues mencions de passada perquè les esperis amb el concepte clar: les sals (valors aleatoris públics que fan únic cada hash de contrasenya) es desenvolupen al mòdul 3, i els IV/nonces (valors aleatoris o únics que fan únic cada xifratge) al mòdul 2. Tots dos són consumidors directes del que hem après avui: es generen amb el CSPRNG, encara que —a diferència de les claus— no són secrets.

Errors Comuns i Consells

  • Fer servir random per a qualsevol cosa de seguretat. L'error número u en Python. Tokens, codis de verificació, contrasenyes temporals, "barrejar" opcions d'un CAPTCHA: tot això és secrets. Cerca recomanada al teu codi: grep -rn "import random" i revisar cada ús.
  • Sembrar amb el temps (seed(time.time())) creient que afegeix seguretat. És exactament el contrari: converteix la llavor en endevinable. Els CSPRNG no es sembren des de l'aplicació.
  • Jutjar l'entropia per l'aspecte. Un valor llarg i "lleig" pot tenir 10 bits d'entropia si el procés que el va generar tenia poques possibilitats. Pregunta sempre pel procés, no per l'aspecte.
  • Reduir l'entropia després de generar-la. Generar 32 bytes perfectes i després quedar-se amb token[:6] "perquè el codi càpiga a l'SMS" deixa 6 caràcters. De vegades és un compromís necessari (codis OTP de 6 dígits), però aleshores s'han de compensar amb límits d'intents i caducitat estrictes.
  • Implementar el teu propi generador ("barrejo la marca de temps amb el PID i li faig un hash"). És l'error de Netscape el 1995. El sistema operatiu ho fa bé; fes-lo servir.
  • Consell: estandarditza a l'equip un helper únic (p. ex. secrets.token_urlsafe(32)) per a tots els tokens; les revisions de codi es tornen trivials: qualsevol altra font d'aleatorietat és una bandera vermella.

Exercicis

Exercici 1. Calcula (o raona) els bits d'entropia de cada valor i ordena'ls de més feble a més fort: (a) un PIN de 4 dígits; (b) un codi de 6 lletres triades uniformement d'un alfabet de 32 símbols amb secrets.choice; (c) secrets.token_hex(16); (d) un "token" format per la marca de temps Unix en segons del moment de la petició (l'atacant coneix l'hora amb un marge de ±1 hora). Pista: entropia = log2(possibilitats); per a (b), cada símbol d'un alfabet de 32 aporta log2(32) = 5 bits.

Exercici 2. En el codi heretat de MediNube apareix aquesta funció per generar contrasenyes temporals del personal de les clíniques. Identifica tots els problemes i reescriu-la correctament amb secrets:

import random, string

def contrasenya_temporal():
    random.seed()  # "perquè sigui aleatori"
    return "".join(random.choice(string.ascii_lowercase) for _ in range(8))

Exercici 3. MediNube vol identificadors no endevinables per a les URL de documents compartits (/doc/<id>), d'uns 22 caràcters aptes per a URL. (a) Escriu la funció. (b) Calcula quants bits d'entropia té la teva proposta. (c) Si MediNube genera un milió d'identificadors, hi ha risc apreciable de col·lisió (dos documents amb el mateix id)? Raona-ho amb ordres de magnitud.

Solucions

Solució 1. (a) PIN de 4 dígits: 10^4 = 10.000 possibilitats ≈ 13,3 bits — trivial. (d) Marca de temps amb marge de ±1 hora: 7.200 possibilitats ≈ 12,8 bits — trivial (i amb estructura!). (b) 6 símbols × 5 bits = 30 bits ≈ 10^9 possibilitats — feble per a un token atacable fora de línia, tolerable només amb límit estricte d'intents en línia. (c) token_hex(16) = 16 bytes = 128 bits — inabastable. Ordre: d < a < b < c. Nota: (d) és el pitjor tot i "semblar" un número gran — l'aspecte enganya, el procés mana.

Solució 2. Problemes: (1) fa servir random, un PRNG predictible, per a un valor de seguretat; (2) random.seed() sense argument no arregla res — la sortida continua sent predictible si es recupera l'estat, i el comentari revela una creença falsa; (3) només minúscules: 26^8 ≈ 2 × 10^11 ≈ 37,6 bits — un atac fora de línia l'esgota ràpidament; (4) 8 caràcters és curt per a l'alfabet triat. Versió correcta:

import secrets, string

def contrasenya_temporal(longitud: int = 16) -> str:
    alfabet = string.ascii_letters + string.digits  # 62 símbols
    return "".join(secrets.choice(alfabet) for _ in range(longitud))

16 caràcters × log2(62) ≈ 95 bits: sobrat per a una contrasenya temporal, que a més ha de caducar aviat i forçar el canvi en el primer accés. (Com emmagatzemar-la de forma segura és el tema de la lliçó 03-03.)

Solució 3. (a) L'eina exacta per a "aleatori + apte per a URL" ja la coneixem:

import secrets

def id_document() -> str:
    return secrets.token_urlsafe(16)   # 16 bytes -> ~22 caràcters URL-safe

(b) L'entropia la donen els bytes, no els caràcters: 16 bytes = 128 bits. (c) Risc de col·lisió: amb n identificadors en un espai de 2^128, la probabilitat que hi hagi alguna col·lisió és aproximadament n²/2^129 (paradoxa de l'aniversari, que reapareixerà al mòdul 3 amb els hashes). Amb n = 10^6: (10^6)² / 2^129 ≈ 10^12 / 6,8 × 10^38 ≈ 10^-27 — indistingible de zero a qualsevol efecte pràctic. Es poden generar sense comprovar la unicitat, encara que una restricció UNIQUE a la base de dades com a cinturó de seguretat mai no sobra.

Conclusió

Ja coneixes el fonament de tot l'edifici: la seguretat criptogràfica es mesura en bits d'entropia, que compten les possibilitats des de la perspectiva de l'atacant; els PRNG com random són predictibles per disseny i queden desterrats de qualsevol ús de seguretat; els CSPRNG del sistema operatiu (alimentats amb soroll físic i exposats via /dev/urandom i getrandom()) són l'única font legítima, i en Python es consumeixen amb secrets i os.urandom. Has vist que 128 bits posen la força bruta fora de l'abast de la física, has generat material per a futures claus amb os.urandom(32) i has substituït el token de recuperació vulnerable de MediNube per secrets.token_urlsafe(32).

Amb això queda una pregunta a l'aire: si l'algorisme pot ser públic i tota la seguretat rau en la clau... per què exactament és això un avantatge i no una imprudència? La resposta té nom propi —el principi de Kerckhoffs— i d'ella es deriven les regles d'or que governaran tot el curs. És el tema de l'última lliçó d'aquest mòdul.

© Copyright 2026. Tots els drets reservats