Encriptació

Com encriptar i desencriptar dades a Laravel: APP_KEY, la façana Crypt, casts encriptats, rotació de claus i bones pràctiques.

Què és l'encriptació?#

L'encriptació és el procés de transformar dades llegibles (text pla) en dades il·legibles (text xifrat) utilitzant una clau secreta. Només qui posseeix la clau pot revertir el procés i llegir les dades originals. A diferència del hashing (que és irreversible), l'encriptació és un procés reversible: xifres dades que necessitaràs llegir posteriorment.

Laravel utilitza l'encriptació per a diversos propòsits interns: xifrar les cookies de sessió, signar les URLs temporals, protegir les dades serialitzades i xifrar els valors de configuració sensibles. A més, posa a disposició del desenvolupador la mateixa infraestructura d'encriptació a través de la façana Crypt i els casts d'Eloquent.

L'encriptació de Laravel utilitza OpenSSL amb l'algoritme AES-256-CBC (o AES-128-CBC per a claus de 16 bytes). AES (Advanced Encryption Standard) és l'estàndard de xifrat simètric més utilitzat al món, aprovat pel govern dels Estats Units per a informació classificada. L'encriptació de Laravel inclou un MAC (Message Authentication Code) que garanteix que les dades xifrades no han estat manipulades. Si algú modifica el text xifrat, la desencriptació falla en lloc de retornar dades corruptes.

La clau d'aplicació (APP_KEY)#

Tota l'encriptació de Laravel depèn d'una única clau secreta: l'APP_KEY. Aquesta clau es genera automàticament quan crees un projecte nou amb laravel new i es guarda al fitxer .env:

APP_KEY=base64:wN5Q8r3b+X2pFaHgVfXkZm1E9qJ5fL/wOx6pK0jRs2g=

Si per algun motiu l'APP_KEY no existeix (per exemple, si has clonat un repositori sense el fitxer .env), la pots generar amb:

php artisan key:generate

Aquesta comanda genera una clau aleatòria de 32 bytes (256 bits), la codifica en base64 i l'escriu al fitxer .env. La clau també s'utilitza al fitxer config/app.php:

// config/app.php
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',

L'APP_KEY és el secret més important de la teva aplicació. Si es compromet, un atacant pot desencriptar totes les dades xifrades (cookies de sessió, tokens, dades de la BD), forjar sessions i suplantar qualsevol usuari. Mai la comparteixis, mai la comitis al control de versions i mai la reutilitzis entre entorns.

Conseqüències de canviar l'APP_KEY#

Canviar l'APP_KEY té conseqüències importants que cal entendre:

  • Sessions: totes les sessions actives s'invaliden immediatament. Tots els usuaris hauran de tornar a iniciar sessió.
  • Cookies xifrades: qualsevol cookie xifrada amb la clau anterior es torna il·legible.
  • Dades encriptades a la BD: si has xifrat dades a la base de dades (amb Crypt::encrypt() o el cast encrypted), aquestes dades es tornen irrecuperables amb la nova clau.
  • URLs signades: les URLs temporals signades amb la clau anterior deixen de funcionar.

Per això, canviar l'APP_KEY és una operació que requereix planificació, especialment si tens dades encriptades a la base de dades. Laravel 11 introdueix la rotació de claus per facilitar aquest procés.

Encriptar i desencriptar dades#

La façana Crypt proporciona una interfície senzilla per xifrar i desxifrar dades:

use Illuminate\Support\Facades\Crypt;
 
// Encriptar una cadena de text
$encrypted = Crypt::encryptString('Número de targeta: 4242-4242-4242-4242');
 
// El resultat és una cadena codificada en base64
// eyJpdiI6InVTMHBhMmN...molt_llarg...
 
// Desencriptar
$decrypted = Crypt::decryptString($encrypted);
// "Número de targeta: 4242-4242-4242-4242"

Encriptar objectes i arrays#

Si necessites encriptar dades que no són strings (arrays, objectes, números), utilitza encrypt() i decrypt() (sense el sufix String). Aquests mètodes serialitzen automàticament les dades abans de xifrar-les i les deserialitzen en desxifrar-les:

use Illuminate\Support\Facades\Crypt;
 
// Encriptar un array
$preferences = [
    'theme' => 'dark',
    'language' => 'ca',
    'notifications' => true,
];
 
$encrypted = Crypt::encrypt($preferences);
 
// Desencriptar retorna l'array original
$decrypted = Crypt::decrypt($encrypted);
// ['theme' => 'dark', 'language' => 'ca', 'notifications' => true]

Gestió d'errors#

Si intentes desencriptar dades corruptes o dades xifrades amb una clau diferent, Laravel llança una excepció DecryptException. Has de capturar-la per evitar que l'aplicació falli:

use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
 
try {
    $decrypted = Crypt::decryptString($encryptedValue);
} catch (DecryptException $e) {
    // La dada no s'ha pogut desencriptar
    // Pot ser corrupte, manipulada o xifrada amb una clau diferent
    Log::warning('Error de desencriptació', [
        'error' => $e->getMessage(),
    ]);
 
    return null;
}

Aquesta gestió d'errors és especialment important quan desencriptes dades de la base de dades que podrien haver estat xifrades amb una clau anterior (si l'APP_KEY ha canviat) o dades que arriben de fonts externes.

Casts encriptats d'Eloquent#

La manera més elegant d'encriptar dades a la base de dades és utilitzar els casts d'Eloquent. En lloc de cridar manualment Crypt::encrypt() i Crypt::decrypt() cada vegada que guardes o llegeixes un valor, defineixes el cast al model i Eloquent s'encarrega automàticament:

namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    protected $casts = [
        // Encripta/desencripta automàticament com a string
        'api_token' => 'encrypted',
 
        // Encripta/desencripta com a array (JSON serialitzat)
        'preferences' => 'encrypted:array',
 
        // Encripta/desencripta com a col·lecció
        'metadata' => 'encrypted:collection',
 
        // Encripta/desencripta com a objecte
        'settings' => 'encrypted:object',
    ];
}

Amb aquesta configuració, el xifrat és completament transparent:

// Guardar: Eloquent encripta automàticament abans de guardar
$user = new User();
$user->api_token = 'sk_live_abc123def456';
$user->preferences = ['theme' => 'dark', 'lang' => 'ca'];
$user->save();
 
// A la BD es guarda el text xifrat (il·legible)
 
// Llegir: Eloquent desencripta automàticament en llegir
$user = User::find(1);
echo $user->api_token;
// "sk_live_abc123def456"
 
echo $user->preferences['theme'];
// "dark"

Columnes encriptades i cerques#

Un aspecte important a tenir en compte: les columnes encriptades no es poden cercar amb consultes SQL convencionals. Com que cada vegada que encriptes el mateix valor el resultat és diferent (per la IV aleatòria), no pots fer:

// Això NO funciona amb columnes encriptades
User::where('api_token', 'sk_live_abc123def456')->first();

Si necessites cercar per un valor encriptat, has d'utilitzar un enfocament alternatiu com guardar un hash (no reversible) del valor en una columna separada per a cerques, i el valor encriptat en una altra columna per a lectura:

// Migració
Schema::table('users', function (Blueprint $table) {
    $table->string('api_token_hash')->index();     // Per cercar
    $table->text('api_token_encrypted');            // Per llegir
});
 
// Model
class User extends Model
{
    protected $casts = [
        'api_token_encrypted' => 'encrypted',
    ];
 
    public function setApiTokenAttribute(string $token): void
    {
        $this->attributes['api_token_hash'] = hash('sha256', $token);
        $this->attributes['api_token_encrypted'] = $token; // El cast l'encriptarà
    }
}
 
// Cercar per hash, llegir el valor encriptat
$user = User::where('api_token_hash', hash('sha256', $token))->first();
$originalToken = $user->api_token_encrypted; // Desencriptat automàticament

Rotació de claus#

La rotació de claus és el procés de canviar l'APP_KEY sense perdre l'accés a les dades encriptades amb la clau anterior. Laravel permet configurar claus anteriors que s'utilitzen per intentar desencriptar dades quan la clau actual falla:

APP_KEY=base64:nova_clau_generada_recentment...
APP_PREVIOUS_KEYS="base64:clau_anterior_1...,base64:clau_anterior_2..."
// config/app.php
'key' => env('APP_KEY'),
'previous_keys' => array_filter(explode(',', env('APP_PREVIOUS_KEYS', ''))),

Amb aquesta configuració, quan Laravel intenta desencriptar una dada, primer prova amb la clau actual. Si falla, prova amb les claus anteriors en ordre. Quan llegeix una dada xifrada amb una clau antiga, la desencripta correctament i pots opcionalment tornar-la a xifrar amb la clau nova.

Procés de rotació#

El procés recomanat per rotar l'APP_KEY és:

# 1. Guardar la clau actual com a clau anterior
# Afegir la clau actual a APP_PREVIOUS_KEYS a .env
 
# 2. Generar una nova clau
php artisan key:generate
 
# 3. Desplegar el canvi
# Les dades antigues es desencriptaran amb la clau anterior
# Les dades noves es xifrarán amb la nova clau
 
# 4. (Opcional) Re-encriptar les dades existents amb la nova clau
php artisan encrypt:rekey

Encriptar fitxers de configuració#

En entorns on no vols exposar variables d'entorn (per exemple, en desplegaments on no pots modificar .env), Laravel permet xifrar tot el fitxer .env:

# Encriptar el fitxer .env
php artisan env:encrypt
 
# Encriptar amb una clau específica
php artisan env:encrypt --key=base64:clau_de_xifrat...
 
# Encriptar un entorn específic
php artisan env:encrypt --env=production
 
# Desencriptar
php artisan env:decrypt
 
# Desencriptar amb la clau
php artisan env:decrypt --key=base64:clau_de_xifrat...

Això crea un fitxer .env.encrypted que pots cometre al control de versions de manera segura. En producció, Laravel desencripta el fitxer utilitzant la clau proporcionada com a variable d'entorn del sistema operatiu (LARAVEL_ENV_ENCRYPTION_KEY).

Quan encriptar i quan no#

L'encriptació afegeix complexitat i cost computacional. No totes les dades necessiten ser encriptades. Algunes directrius:

Encriptar:

  • Tokens d'API i claus de serveis externs
  • Dades personals sensibles (números d'identificació, dades mèdiques)
  • Informació de pagament (tot i que és millor no guardar-la i usar un gateway)
  • Qualsevol dada que, si es filtra, podria causar dany als usuaris

No encriptar:

  • Contrasenyes: utilitza hashing (bcrypt/Argon2), no encriptació
  • Dades que necessites cercar per valor a la BD (noms, emails per a login)
  • Dades públiques (títols d'articles, descripcions)
  • Dades que necessiten ser indexades per a consultes ràpides

La regla general és: si necessites poder llegir la dada original i és sensible, encripta-la. Si no necessites llegir-la (com les contrasenyes), fes-ne hash. Si no és sensible, no la xifris innecessàriament.