
Cosa c'è dietro la Cifratura/Decifratura di Laravel
Roberto Gallea • 20 settembre 2022
NOTA: Traduzione da robertogallea.com
Il mio sistema è sicuro, usa la cifratura.
Lo avrai sicuramente sentito/detto di tanto in tanto. Sicuro che lo è, ma perchè e come è sicuro? Lo sai davvero?
Fondamentali della cifratura/decifratura di Laravel
La cifratura/decifratura di Laravel è basata sulla classe Illuminate\Encryption\Encrypter
, che è costruita passando una chiave di cifratura ed un cifrario (cioè l'algoritmo di cifratura):
__construct($key, $cipher = 'AES-128-CBC')
Supporta (fra gli altri) i seguenti metodi principali:
encrypt($value, $serialize = true)
decrypt($payload, $unserialize = true)
i quali, nemmeno a dirlo, sono usati per cifrare e decifrare dati.
$encrypter = new Illuminate\Encryption\Encrypter('1234567812345678', 'AES-128-CBC');
$encrypted = $encrypter->encrypt('Hello world');
dump($encrypted);
// stampa qualcosa di simile a "eyJpdiI6ImdMd2dWcW5jMXBrUDBranRJZXQ5MEE9PSIsInZhbHVlIjoiNnhTODBSclB3ZVp3SFRRUWFWTHpReFQwYWQ1aXVmTmhXOXV5WHM2TzR1WT0iLCJtYWMiOiIwODQyZDhiMzZlNDQwZTZjYTRiYmI2MGE0MTgzNzk5NGNkZTU1Yzc5NDIyYzdjYmYwNzk2ZTA5MGNjYjc4MGYzIn0="
$decrypted = $encrypter->decrypt($encrypted);
dump($decrypted);
// stampa di nuovo "Hello world"
Magnifico! Già solo questo è sufficiente per usarlo nel migliore dei modi.
Tuttavia, sei vuoi sapere cosa succede internamente, continua a leggere.
ATTENZIONE! Considera che i risultati non saranno esattamente gli stessi, dato che alcuni valori sono generati casualmente, e dunque cambiano ad ogni esecuzione.
Come funziona la cifratura
Il cifratore di Laravel attualmente usa OpenSSL per effettuare la cifratura AES-256 e AES-128. Inoltre usa la protezione Message Authentication Code (MAC), un meccanismo per assicurare che i dati non vengano manomessi dopo la cifratura.
Cosa c'è nel risultato?
Riprendendo l'esempio precedente, potresti pensare che la stringa cifrata
"eyJpdiI6ImdMd2dWcW5jMXBrUDBranRJZXQ5MEE9PSIsInZhbHVlIjoiNnhTODBSclB3ZVp3SFRRUWFWTHpReFQwYWQ1aXVmTmhXOXV5WHM2TzR1WT0iLCJtYWMiOiIwODQyZDhiMzZlNDQwZTZjYTRiYmI2MGE0MTgzNzk5NGNkZTU1Yzc5NDIyYzdjYmYwNzk2ZTA5MGNjYjc4MGYzIn0="
sia essa stessa la versione cifrata dell'input. Questo è senz'altro vero, ma c'è di più da sapere.
Essa è infatti la conversione base64 di una stringa. "Che stringa?" potresti chiederti... E puoi ottenere una risposta semplicemente eseguendo il seguente codice:
$encrypted = $encrypter->encrypt('Hello world');
$decodedEncrypted = base64_decode($encrypted);
il quale produce una stringa json simile alla seguente:
{
"iv":"gLwgVqnc1pkP0kjtIet90A==",
"value":"6xS80RrPweZwHTQQaVLzQxT0ad5iufNhW9uyXs6O4uY=",
"mac":"0842d8b36e440e6ca4bbb60a41837994cde55c79422c7cbf0796e090ccb780f3"
}
Già... Adesso è ancora meno chiaro... Di che si tratta?
Questo documento json è composto dalle tre parti principali della cifratura:
value
: i dati cifrati veri e propri, codificati in base64iv
: l'Initialization Vector, una sequenza di dati di lunghezza fissa generati casualmente, iniettati ad ogni esecuzione, per prevenire attacchi basati sulla semantica, vedi (Initialization vector - Wikipedia per maggiori dettagli). Anche questo è codificato in base64mac
: il Message Authentication Code, una firma usata per identificare eventuali manomissioni delvalue
, generato effettuando l'hash divalue
eiv
. Esso è rappresentato mediante una stringa in formato esadecimale.
Fai caso che sia iv
che value
vanno codificati in base64 poichè sono composti da byte generici e potrebbero contenere caratteri non stampabili.
Come funziona la cifratura - uno sguardo al codice
Per capire come venga generato il payload json, diamo un'occhiata più da vicino al metodo encrypt()
:
public function encrypt($value, $serialize = true)
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
$value = \openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher, $this->key, 0, $iv
);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new EncryptException('Could not encrypt the data.');
}
return base64_encode($json);
}
Guardando il codice, vengono realizzati 5 passi:
- L'Initialization Vector viene generato alla riga
3
generando 128 o 256 bit (in base al cifrario usato) di dati casuali - Il valore cifrato viene generato nelle righe
5-8
eseguendo OpenSSL sulla versione (eventualmente) serializzata dei dati in chiaro, usando il cifrario, la chiave di cifratura e l'IV scelti. Nota che il risultato viene codificato in base64 - Il MAC viene generato tramite il metodo
hash()
, a cui vanno dati in pasto l'IV base64 ed il valore cifrato. L'hashing è definito come:
protected function hash($iv, $value)
{
return hash_hmac('sha256', $iv.$value, $this->key);
}
cioè come l'hashing SHA256 della concatenazione dell'IV e del valore cifrato, usando la chiave di cifratura fornita.
4. Un array contenente iv
, value
e mac
viene generato e convertito in formato json (riga 16
)
5. Il json viene codificato in base64 ed infine restituito (riga 22
)
Come funziona la decifratura - in dettaglio
Per capire come i dati originali vengano decifati, diamo un'occhiata da vicino al metodo decrypt()
:
public function decrypt($payload, $unserialize = true)
{
$payload = $this->getJsonPayload($payload);
$iv = base64_decode($payload['iv']);
// Here we will decrypt the value. If we are able to successfully decrypt it
// we will then unserialize it and return it out to the caller. If we are
// unable to decrypt this value we will throw out an exception message.
$decrypted = \openssl_decrypt(
$payload['value'], $this->cipher, $this->key, 0, $iv
);
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
return $unserialize ? unserialize($decrypted) : $decrypted;
}
Guardando il codice, 5 passi vengono eseguiti:
Il payload json viene estratto alla riga
3
. Durante l'estrazione, esso viene validato verificando che:1.1. Sia in forma di array
1.2. Contenga i campiiv
,value
emac
.
1.3. La lunghezza diiv
sia compatibile con i requisiti del cifrario scelto
1.4. Ilmac
sia valido- I dati vengono decifrati usando OpenSSL (righe
5-12
) - Il risultato viene (eventualmente) deserializzato e ritornato.
Perchè è sicuro?
Questo schema fornisce sicurezza fin tanto che la chiave di cifratura viene mantenuta segreta. Vediamo perchè:
- fiducia: il messaggio in chiaro può essere decifrato solo da chi conosce la chiave segreta
- integrità: se il valore viene modificato, la decifratura fallisce. Se iv ed il valore vengono entrambi modificati, il messaggio potrebbe essere potenzialmente decifrabile, ma la protezione MAC identificherà la manomissione e la decifratura fallirà. In ogni caso, cambiare una combinazione di iv e/o del valore e/o del MAC, farà sì che la decifratura fallisca a causa della corruzione del payload json.
- L'unico modo per ingannare la protezione MAC è conoscendo la chiave di cifratura, che permetterebbe la forgiatura di nuovi payload cifrati validi.
Se non siete ancora convinti, proviamo: crea un diverso messaggio cifrato:
$encrypted2 = $encrypter->encrypt('Hello hacker');
$decodedEncrypted2 = json_decode(base64_decode($encrypted2), true);
dump('DECODED ENCRYPTED 2: ');
var_dump($decodedEncrypted2);
Adesso, prova a manomettere uno o più dei tre valori e prova a decifrare il risultato ottenuto.
// scambio dei dati cifrati e tentativo di decifratura
try {
$tampered = $decodedEncrypted;
$tampered['value'] = $decodedEncrypted2['value'];
$encrypter->decrypt(base64_encode(json_encode($tampered)));
} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
dump($exception->getMessage());
}
// scambio degli iv e tentativo di decifratura
try {
$tampered = $decodedEncrypted;
$tampered['iv'] = $decodedEncrypted2['iv'];
$encrypter->decrypt(base64_encode(json_encode($tampered)));
} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
dump($exception->getMessage());
}
// scambio dei MAC e tentativo di decifratura
try {
$tampered = $decodedEncrypted;
$tampered['mac'] = $decodedEncrypted2['mac'];
$encrypter->decrypt(base64_encode(json_encode($tampered)));
} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
dump($exception->getMessage());
}
In tutti e tre i casi, il control MAC fallisce, così come la decifratura ed una DecryptException
viene lanciata.
Conclusione
Adesso sai più in dettaglio come la cifratura di Laravel funzioni all'interno. Niente è cambiato del modo di usarla, ma hai acquisito più fiducia negli strumenti che usi. Inoltre, adesso sei in grado di giustificare con i tuoi clienti "come" il tuo sistema è sicuro.