Si haguéssim de triar la confusió que més vulnerabilitats reals causa entre desenvolupadors, seria aquesta: tractar la codificació (com Base64) o l'ofuscació (com ROT13) com si fossin xifratge. Les tres transformen dades i les tres produeixen "text estrany" a simple vista, però només el xifratge ofereix una garantia de seguretat recolzada per una clau secreta. En aquesta lliçó establirem la diferència amb rigor, practicarem amb les codificacions que veuràs constantment (Base64, hexadecimal, URL-encoding, UTF-8) fent servir la llibreria estàndard de Python, i analitzarem un cas de revisió de codi a MediNube on algú va "protegir" dades sanitàries amb Base64. Dominar aquesta distinció és requisit per a tot el que ve.
Contingut
- Tres transformacions que es confonen
- Codificar: representar informació
- Text i bytes: UTF-8, el primer pas sempre
- Base64 i hexadecimal en Python
- URL-encoding i Base64 URL-safe
- Ofuscar: dificultar sense clau
- Xifrar: protegir amb una clau secreta
- Taula comparativa i criteri de decisió
- Cas pràctic: revisió de codi a MediNube
Tres transformacions que es confonen
Les tres operacions transformen una entrada en una sortida diferent, però amb propòsits radicalment diferents:
- Codificar (encode): canviar la representació de les dades perquè es puguin transportar o emmagatzemar en un mitjà concret. No hi ha secret: qualsevol que conegui el format (que és públic i estàndard) recupera l'original. Exemples: UTF-8, Base64, hexadecimal, URL-encoding.
- Ofuscar: dificultar la lectura casual, sense clau secreta. Qui conegui (o endevini) el mètode recupera l'original sense esforç. Exemples: ROT13, minificació de JavaScript, "invertir la cadena".
- Xifrar (encrypt): transformar les dades amb un algorisme i una clau secreta, de manera que recuperar l'original sense la clau sigui computacionalment inviable, encara que l'atacant conegui perfectament l'algorisme.
graph TD
T[Transformació de dades] --> COD[Codificació]
T --> OFU[Ofuscació]
T --> CIF[Xifratge]
COD --> COD1["Objectiu: representar/transportar<br/>Es reverteix: coneixent el format públic"]
OFU --> OFU1["Objectiu: dificultar la lectura casual<br/>Es reverteix: coneixent el mètode"]
CIF --> CIF1["Objectiu: confidencialitat<br/>Es reverteix: NOMÉS amb la clau secreta"]
La pregunta discriminant és una de sola: què necessita algú per revertir la transformació? Si la resposta és "res de secret, només conèixer el format o el mètode", no és xifratge. Punt.
Codificar: representar informació
Els ordinadors només gestionen bytes. "Codificar" és acordar com representem alguna cosa (text, una imatge, una clau criptogràfica) com a bytes, o com representem bytes en un mitjà que no els admet tots. Dues situacions típiques:
- De text a bytes: el text "Maria" s'ha de convertir en bytes per desar-lo o transmetre'l → UTF-8.
- De bytes arbitraris a text segur: una clau criptogràfica són 32 bytes aleatoris que inclouen valors no imprimibles; per ficar-los en un JSON, una URL o una variable d'entorn necessitem representar-los amb caràcters "innocents" → Base64 o hexadecimal.
Aquesta segona situació explica per què Base64 apareix per tot arreu en criptografia: les sortides de xifrar, signar o hashejar són bytes arbitraris, i gairebé tots els formats d'intercanvi (JSON, HTTP, XML, certificats PEM, JWT) són textuals. Base64 és l'"embalatge de transport" d'aquests bytes. Veure Base64 a prop de la criptografia és normal; creure que Base64 és la criptografia és l'error.
Text i bytes: UTF-8, el primer pas sempre
En Python 3 la distinció entre str (text) i bytes és estricta, i les funcions criptogràfiques sempre treballen amb bytes. La conversió es fa amb UTF-8:
text = "Historial de la pacient Ana Pérez — Clínica Sol"
dades = text.encode("utf-8") # str -> bytes
print(dades)
# b'Historial de la pacient Ana P\xc3\xa9rez \xe2\x80\x94 Cl\xc3\xadnica Sol'
recuperat = dades.decode("utf-8") # bytes -> str
print(recuperat == text) # TrueObserva el resultat:
- El prefix
b'...'indica un objectebytes. - Els caràcters ASCII ("Historial", espais) ocupen 1 byte i es mostren tal qual.
- La
ées converteix en dos bytes (\xc3\xa9) i la ratlla—en tres (\xe2\x80\x94): UTF-8 fa servir entre 1 i 4 bytes per caràcter. Per això "longitud del text" i "longitud en bytes" no coincideixen — detall que importarà en parlar de mides de claus i de missatges. .decode("utf-8")reverteix l'operació exactament. No hi ha cap secret: UTF-8 és un estàndard públic.
Regla pràctica per a tot el curs: abans de qualsevol operació criptogràfica, text → bytes amb .encode("utf-8"); després de desxifrar, bytes → text amb .decode("utf-8").
Base64 i hexadecimal en Python
Base64
Base64 representa bytes fent servir només 64 caràcters segurs (A-Z, a-z, 0-9, +, /, més = com a farciment). Cada 3 bytes d'entrada es converteixen en 4 caràcters de sortida, així que el resultat ocupa un ~33 % més.
import base64
dades = "Pacient: Ana Pérez, al·lèrgia: penicil·lina".encode("utf-8")
codificat = base64.b64encode(dades)
print(codificat)
# b'UGFjaWVudDogQW5hIFDDqXJleiwgYWzCt2zDqHJnaWE6IHBlbmljaWzCt2xpbmE='
decodificat = base64.b64decode(codificat)
print(decodificat.decode("utf-8"))
# Pacient: Ana Pérez, al·lèrgia: penicil·linaPunt per punt:
b64encoderepbytesi retornabytes(els caràcters Base64 en ASCII). Si necessites unstrper a un JSON, afegeix.decode("ascii").- El
=final és farciment (padding): com que l'entrada es processa en grups de 3 bytes, si el total no és múltiple de 3 la sortida es completa amb=perquè sigui múltiple de 4. b64decodereverteix l'operació sense necessitar res de secret. Qualsevol persona, eina o atacant decodifica Base64 a l'instant; fins i tot els navegadors porten una funció per fer-ho (atob).
Com reconèixer Base64 a simple vista? Cadenes de lletres majúscules/minúscules i dígits, potser amb +//, longitud múltiple de 4 i sovint un o dos = al final. Reconèixer-ho és una habilitat útil en revisions de seguretat.
Hexadecimal
L'hexadecimal representa cada byte com dos caràcters 0-9a-f. Ocupa el doble que els bytes originals (pitjor que Base64) però és més llegible i molt comú per a hashes, claus i identificadors:
import binascii
dades = "Sol".encode("utf-8")
en_hex = binascii.hexlify(dades)
print(en_hex) # b'536f6c' (S=0x53, o=0x6f, l=0x6c)
print(binascii.unhexlify(en_hex)) # b'Sol'
# Alternativa moderna sense importar res:
print(dades.hex()) # '536f6c'
print(bytes.fromhex("536f6c")) # b'Sol'hexlify/unhexlify(mòdulbinascii) i els mètodesbytes.hex()/bytes.fromhex()fan el mateix; els segons són més idiomàtics en el Python modern.- De nou: transformació pública, reversible per qualsevol, cap seguretat.
| Codificació | Alfabet | Sobrecost de mida | Ús típic |
|---|---|---|---|
| UTF-8 | — (text ↔ bytes) | 1-4 bytes per caràcter | Tot el text |
| Base64 | 64 caràcters + = |
+33 % | Transport de bytes en JSON, HTTP, PEM, JWT |
| Hexadecimal | 16 caràcters | +100 % | Hashes, claus, depuració |
| URL-encoding | %XX |
Variable | Dades dins d'URLs |
URL-encoding i Base64 URL-safe
Les URL només admeten un subconjunt de caràcters. L'URL-encoding (o percent-encoding) substitueix els problemàtics per % seguit del seu valor hexadecimal:
from urllib.parse import quote, unquote consulta = "cognom=Pérez&clinica=Clínica Sol" print(quote(consulta)) # cognom%3DP%C3%A9rez%26clinica%3DCl%C3%ADnica%20Sol print(unquote(quote(consulta)) == consulta) # True
quotecodifica:=passa a%3D, l'espai a%20, i laéa%C3%A9(els seus dos bytes UTF-8, cadascun amb el seu%XX! — les codificacions s'apilen unes sobre les altres).unquotedecodifica. Públic i reversible, com sempre.
Base64 estàndard fa servir + i /, que xoquen amb l'URL-encoding. Per això existeix la variant Base64 URL-safe (fa servir - i _), omnipresent en tokens web i JWT (mòdul 6):
import base64, os token_bytes = os.urandom(16) # 16 bytes aleatoris (ho veurem a 01-03) print(base64.urlsafe_b64encode(token_bytes)) # p. ex. b'x3K9-fT_2mQlZ0aVb1cN4g==' — apte per enganxar en una URL
Ofuscar: dificultar sense clau
L'ofuscació busca que la dada no es llegeixi "d'una ullada", però no hi ha clau secreta: conèixer el mètode n'hi ha prou per revertir-la. L'exemple canònic és ROT13, un Cèsar amb desplaçament 13 (aplicar-lo dues vegades retorna l'original):
import codecs missatge = "Resultat reservat" ofuscat = codecs.encode(missatge, "rot13") print(ofuscat) # Erfhygng erfreing print(codecs.decode(ofuscat, "rot13")) # Resultat reservat
Fixa't en el detall delator: ROT13 és al mòdul codecs de Python, al costat de les codificacions. Ni el mateix llenguatge ho considera seguretat. Té usos legítims l'ofuscació? Sí, però cap és de seguretat forta:
- Evitar spoilers o destripar trames en fòrums (ROT13 clàssic).
- Minificar/ofuscar JavaScript: dificulta (una mica) copiar la lògica de negoci del frontend i redueix la mida; qualsevol eina de pretty-printing i una mica de paciència ho reverteixen.
- Que una dada no aparegui en clar davant d'un
grepcasual.
L'ofuscació pot afegir fricció sobre mesures reals; el problema és fer-la servir en lloc de mesures reals. Recuperarem aquesta idea com a "seguretat per obscurantisme" en la lliçó 01-04.
Xifrar: protegir amb una clau secreta
El xifratge és qualitativament diferent: la transformació depèn d'una clau secreta, i la garantia és que sense ella, recuperar el text en clar és computacionalment inviable encara que l'atacant tingui el text xifrat, conegui l'algorisme al detall i disposi de maquinari massiu. La seguretat no rau en el fet que el mètode sigui desconegut, sinó que l'espai de claus és astronòmic (ho quantificarem a 01-03) i l'algorisme no filtra patrons.
Encara no xifrarem de veritat — els algorismes reals (AES, ChaCha20) són el tema del mòdul 2, amb la llibreria cryptography —, però deixem ja plantejat el contracte:
xifrar(text_en_clar, clau) -> text_xifrat # il·legible sense la clau desxifrar(text_xifrat, clau) -> text_en_clar # només amb LA MATEIXA garantia: la clau
I una conseqüència pràctica que tanca el cercle amb Base64: el text_xifrat són bytes arbitraris, així que quan vegis xifratge real en APIs i fitxers, gairebé sempre viatjarà a més codificat en Base64. Codificació i xifratge no competeixen: es complementen, cadascun en el seu paper.
Taula comparativa i criteri de decisió
| Propietat | Codificació | Ofuscació | Xifratge |
|---|---|---|---|
| Propòsit | Representar / transportar | Dificultar la lectura casual | Confidencialitat |
| Requereix clau secreta? | No | No | Sí |
| Qui la pot revertir? | Qualsevol que conegui el format (públic) | Qualsevol que conegui o dedueixi el mètode | Només qui té la clau |
| Reversible? | Sí, sempre i exactament | Sí (per disseny trivial) | Sí, però només amb la clau |
| Garantia de seguretat | Cap | Cap (només fricció) | Formal i quantificable (bits de seguretat) |
| Exemples | UTF-8, Base64, hex, URL-encoding | ROT13, minificació | AES, ChaCha20 (mòdul 2) |
Criteri de decisió en una frase: si el requisit és que algú no autoritzat no pugui llegir la dada, l'única opció és xifrar. Codifica per transportar; ofusca, si de cas, per molestar; xifra per protegir.
Cas pràctic: revisió de codi a MediNube
Primera setmana a MediNube. Durant una revisió de codi trobes aquest pull request, que respon al requisit "els DNI dels pacients no s'han de desar en clar a la base de dades":
import base64
def protegir_dni(dni: str) -> str:
"""Protegeix el DNI del pacient abans de desar-lo a la base de dades."""
return base64.b64encode(dni.encode("utf-8")).decode("ascii")
def recuperar_dni(dni_protegit: str) -> str:
return base64.b64decode(dni_protegit).decode("utf-8")
# Desat a la taula pacients:
print(protegir_dni("12345678Z")) # MTIzNDU2NzhaL'autor argumenta: "ja no es veu el DNI a la base de dades". Analitzem-ho com a revisors:
- Què fa realment? Codifica en Base64. La columna mostrarà
MTIzNDU2Nzhaen lloc de12345678Z. A simple vista sembla "protegit". - Què necessita un atacant per revertir-ho? Res. Si roba un bolcat de la base de dades, una línia (
base64.b64decode(...)) o qualsevol web de "base64 decode" li retorna tots els DNI. No hi ha clau; no hi ha seguretat. És equivalent a desar la dada en clar amb una disfressa. - És fins i tot pitjor que res? En cert sentit, sí: el nom
protegir_dnidocumenta una protecció que no existeix. En una auditoria, algú podria marcar el requisit com a complert. La falsa seguretat desactiva les alarmes. - Què s'hauria de fer? Xifratge real amb clau gestionada fora de la base de dades — exactament el que construirem al mòdul 2 (xifratge autenticat) i al mòdul 6 (xifratge en repòs i gestió de claus). El comentari de revisió correcte és: "Base64 és codificació, no xifratge: no compleix el requisit de confidencialitat. Bloquejo el PR; ho abordem amb xifratge autenticat i una clau fora de la BD."
Nota de compliment: el DNI és una dada personal i, associat a un context sanitari, el conjunt és especialment sensible sota el RGPD. En un sistema real, decisions com aquesta han de passar per professionals de seguretat i compliment; aquest cas és una simplificació didàctica.
Aquest patró (Base64 presentat com a protecció) apareix en sistemes reals amb una freqüència depriment: en capçaleres "secretes", en configuracions, en tokens casolans. A partir d'avui el detectaràs en segons.
Errors Comuns i Consells
- "Està en Base64, així que està xifrat". L'error estrella. Base64 es decodifica sense clau, sempre, per qualsevol. Si ho pots revertir sense un secret, no és xifratge.
- Confondre el vocabulari anglès: encode/decode (codificar) vs encrypt/decrypt (xifrar). En moltes API conviuen, i llegir
encodepensant "encrypt" causa malentesos reals. - Oblidar que les funcions criptogràfiques volen
bytes. ElTypeError: Unicode-objects must be encoded...et perseguirà si no interioritzesstr.encode("utf-8")→ operar →bytes.decode("utf-8"). - Fer servir Base64 estàndard dins d'URL. Els caràcters
+,/i=es corrompen o requereixen escapament; fes servirbase64.urlsafe_b64encodeper a tokens que viatgin en URL. - Descartar la codificació com a "irrellevant per a la seguretat". Els errors de codificació sí que causen vulnerabilitats (doble decodificació, confusions UTF-8) i els formats criptogràfics reals (PEM, JWT) són capes de Base64 sobre bytes; entendre l'embalatge evita confondre'l amb el contingut.
- Consell: en qualsevol revisió de codi, quan vegis una funció anomenada
protegir*,encriptar*oocultar*, mira a dins. Si no hi ha cap clau secreta involucrada, no protegeix res.
Exercicis
Exercici 1. Classifica cada element com a codificació, ofuscació o xifratge, i justifica-ho en una línia: (a) desar la contrasenya del panell d'administració de MediNube en el codi font com cGFzc3dvcmQxMjM=; (b) el text d'un avís de spoiler transformat amb ROT13; (c) un fitxer d'historial que només es pot obrir amb una clau de 256 bits generada aleatòriament; (d) els bytes \xc3\xa9 representant la lletra é; (e) el JavaScript del frontend de MediNube passat per un minificador.
Exercici 2. Interceptes (en un entorn de proves de MediNube) aquesta cadena enviada per una app antiga: eyJ1c3VhcmkiOiAiYW5hLnBlcmV6IiwgInJvbCI6ICJwYWNpZW50In0=. (a) Escriu el codi Python que la decodifica. (b) Què conté? (c) Quin problema de seguretat implica que l'app faci servir això com a "token de sessió" tal qual?
Exercici 3. Escriu una funció bytes_a_json(dades: bytes) -> str que prepari bytes arbitraris (per exemple, la sortida d'un futur xifratge) per incloure'ls en un camp JSON, i la seva inversa json_a_bytes(camp: str) -> bytes. Comprova-ho amb os.urandom(32). Per què no pots ficar els bytes directament al JSON?
Solucions
Solució 1. (a) Codificació (Base64 de password123): la contrasenya està en clar a efectes pràctics — doble error: credencial en el codi i "protecció" inexistent. (b) Ofuscació: evita la lectura accidental, reversible per qualsevol que conegui ROT13. (c) Xifratge: hi ha clau secreta i sense ella el contingut és inaccessible. (d) Codificació (UTF-8): pura representació de text com a bytes. (e) Ofuscació: dificulta la lectura, però tota la lògica hi és i és recuperable.
Solució 2.
import base64, json
token = "eyJ1c3VhcmkiOiAiYW5hLnBlcmV6IiwgInJvbCI6ICJwYWNpZW50In0="
decodificat = base64.b64decode(token).decode("utf-8")
print(decodificat)
# {"usuari": "ana.perez", "rol": "pacient"}
dades = json.loads(decodificat)(b) Un JSON amb l'usuari i el seu rol. La pista clàssica: les cadenes Base64 que comencen per eyJ són gairebé sempre un JSON codificat ({" codifica a eyJ), una cosa omnipresent en JWT (mòdul 6). (c) El "token" és simplement les dades codificades, sense clau ni verificació: qualsevol pot fabricar un token canviant "rol": "pacient" per "rol": "metge" i recodificant en Base64. No hi ha autenticitat ni integritat. La solució real són tokens signats, que construirem al mòdul 6 després d'aprendre HMAC (mòdul 3) i signatures (mòdul 4).
Solució 3.
import base64, os, json
def bytes_a_json(dades: bytes) -> str:
return base64.b64encode(dades).decode("ascii")
def json_a_bytes(camp: str) -> bytes:
return base64.b64decode(camp)
clau_futura = os.urandom(32)
document = json.dumps({"material": bytes_a_json(clau_futura)})
recuperats = json_a_bytes(json.loads(document)["material"])
assert recuperats == clau_futuraNo pots ficar els bytes directament perquè JSON només admet text Unicode en les seves cadenes, i 32 bytes aleatoris gairebé segur contenen seqüències que no són UTF-8 vàlid (a més, json.dumps rebutja objectes bytes directament). Base64 és exactament el pont estàndard per a això — i per això el veuràs embolicant tot el material criptogràfic del curs.
Conclusió
Ja tens el criteri que separa les tres transformacions: codificar representa (UTF-8, Base64, hex, URL-encoding — públiques i reversibles per qualsevol), ofuscar només molesta (ROT13, minificació — sense clau, sense garantia), i xifrar protegeix amb una clau secreta i una garantia computacional. Has practicat amb base64, binascii i urllib.parse, saps per què Base64 embolica gairebé tot el material criptogràfic sense ser criptografia, i has bloquejat el teu primer PR a MediNube per confondre codificació amb xifratge.
Ara bé, hem dit que tota la força del xifratge rau en la clau secreta... però d'on surt una bona clau? Què fa que un valor sigui imprevisible de debò? Aquesta és la base d'absolutament tota la resta, i és el tema de la lliçó següent: aleatorietat i entropia.
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
