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

  1. Tres transformacions que es confonen
  2. Codificar: representar informació
  3. Text i bytes: UTF-8, el primer pas sempre
  4. Base64 i hexadecimal en Python
  5. URL-encoding i Base64 URL-safe
  6. Ofuscar: dificultar sense clau
  7. Xifrar: protegir amb una clau secreta
  8. Taula comparativa i criteri de decisió
  9. 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:

  1. De text a bytes: el text "Maria" s'ha de convertir en bytes per desar-lo o transmetre'l → UTF-8.
  2. 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)          # True

Observa el resultat:

  • El prefix b'...' indica un objecte bytes.
  • 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·lina

Punt per punt:

  • b64encode rep bytes i retorna bytes (els caràcters Base64 en ASCII). Si necessites un str per 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.
  • b64decode reverteix 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òdul binascii) i els mètodes bytes.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
  • quote codifica: = 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).
  • unquote decodifica. 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 grep casual.

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
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"))   # MTIzNDU2Nzha

L'autor argumenta: "ja no es veu el DNI a la base de dades". Analitzem-ho com a revisors:

  1. Què fa realment? Codifica en Base64. La columna mostrarà MTIzNDU2Nzha en lloc de 12345678Z. A simple vista sembla "protegit".
  2. 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.
  3. És fins i tot pitjor que res? En cert sentit, sí: el nom protegir_dni documenta una protecció que no existeix. En una auditoria, algú podria marcar el requisit com a complert. La falsa seguretat desactiva les alarmes.
  4. 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 encode pensant "encrypt" causa malentesos reals.
  • Oblidar que les funcions criptogràfiques volen bytes. El TypeError: Unicode-objects must be encoded... et perseguirà si no interioritzes str.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 servir base64.urlsafe_b64encode per 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* o ocultar*, 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_futura

No 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.

© Copyright 2026. Tots els drets reservats