Laravel: Crittografare il Database con Eloquent

L

Quando abbiamo la necessità di gestire informazioni sensibili può essere estremamente saggio memorizzare questi dati in forma criptata, per limitare i danni derivanti da un data breach e tenere i nostri dati sensibili al riparo da occhi indiscreti. In questo articolo vedremo come gestire la crittografia utilizzando l’ORM di Laravel: Eloquent.

Il metodo che vedremo in questo breve articolo è abbastanza diffuso e chi già bazzica in rete, alla ricerca delle più disparate soluzioni, avrà sicuramente avuto modo di vederlo. Per tutti gli altri seguirà una guida step by step, che vi consentirà di comprendere appieno questo metodo.

Il funzionamento di Eloquent

Prima di addentrarci nell’implementazione di un metodo, per la crittografia del database, vediamo di gettare alcune basi che ci permettano di capire il funzionamento di Eloquent (seguiranno alcune semplificazioni).

Eloquent è un ORM che sfrutta l’approccio ActiveRecord. ActiveRecord è un design pattern architetturale che ci consente di mappare le nostre tabelle del database su delle apposite subclassi del tipo Illuminate\Database\Eloquent\Model

Con questo approccio la classe rappresenta la nostra tabella ed espone i metodi (statici e non) per l’elaborazione e la modifica dei dati in essa contenuti. L’istanza di questa classe sarà invece il nostro singolo record.

Facciamo subito un esempio pratico: supponiamo di avere, nel nostro database, una tabella di nome “students” (che contiene, per l’appunto, i dati anagrafici di alcuni studenti) e di aver creato il nostro model in app/Student.php:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Student extends Model {
    protected $table = "students";
}

Una volta definito il nostro model potremo manipolare i dati presenti nella nostra base dati nel seguente modo:

//Creo un nuovo record
$student = new \App\Student;
$student->name = "Mario";
$student->last_name = "Rossi";
//Memorizzo il nuovo record nel db
$student->save(); 

//Eseguo una piccola query
$student = \App\Student::where('name', '=', 'Mario')->first();
$student->city = "Roma";
//Aggiorno i valori del record nel db
$student->save(); 

//Prelevo tutti gli studenti presenti nel db
$students = \App\Student::all();

Come potete notare le colonne della tabella “students” sono direttamente mappate sulle proprietà dell’oggetto.

Under the Hood

A questo punto una domanda sorge spontanea: come fa Eloquent a conoscere il nostro db schema (e a mappare, automaticamente, le nostre colonne sugli attributi della classe)? La risposta è semplice: non conosce il db schema e da per scontato che sappiate quello che state facendo.

Per questo motivo se, ad esempio, decidessimo di assegnare alla colonna “pippo” (non presente nel database) un valore, eloquent proverebbe comunque a scrivere questo valore su database, generando un errore “Column not found: 1054 Unknown column pippo in field list”:

Tinker console

Questo succede perché le nostre colonne vengono mappate su un array interno (associazione nome colonna => valore) ed esposte grazie ai magic method __get e __set…

namespace Illuminate\Database\Eloquent;

[..]

abstract class Model implements [...] {
    [...]
    
    /**
     * Dynamically retrieve attributes on the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getAttribute($key);
    }

    /**
     * Dynamically set attributes on the model.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return void
     */
    public function __set($key, $value)
    {
        $this->setAttribute($key, $value);
    }
    
    [...]
}

…che a loro volta invocano i metodi getAttribute e setAttribute, i quali si occupano rispettivamente di prelevare ed inserire il valore da e nell’array $attributes. In realtà questi metodi non si limitano alla sola manipolazione dell’array $attributes ma gestiscono anche:

  • Invocazione di mutator e accessor
  • Serializzazione/deserializzazione dei campi di tipo data
  • Esecuzione dei casting

A questo punto non resta che sfruttare le conoscenze appena acquisite per realizzare un piccolo trait in grado di gestire le colonne che dovranno essere memorizzate in forma criptata.

Prima di cominciare

Se non lo avete ancora fatto generate un nuovo progetto Laravel (scaricate composer se non è già presente sul vostro sistema):

#Creazione di un nuovo progetto Laravel con "composer" installato localmente
php composer.phar create-project --prefer-dist laravel/laravel nomeprogetto
#... o con composer installato globalmente
composer create-project --prefer-dist laravel/laravel nomeprogetto

D’ora in avanti, per tutte le prove, sarà sufficiente usare la tinker console, avviabile con il comando:

php artisan tinker

Tinker console

Per chi non l’avesse mai usata, tinker è una console interattiva che ci permette di scrivere il nostro codice php “live”. Oltre a stampare l’output delle vostre istruzioni, la console stampa a video anche il valore di ritorno:

È facile distinguere i vari output grazie ai caratteri che precedono la riga:

  • “>>>” indica che si tratta di una riga di input
  • =>” indica che si tratta del valore dell’istruzione precedente
  • Quando invece non è preceduta da nulla allora si tratta di un output

Criptare il nostro database

Per questo articolo faremo riferimento ad una tabella ricca di dati sensibili:

Db Model

Riprendiamo quindi la classe Student, vista precedentemente, e apportiamo le dovute modifiche per criptare, ad esempio, l’indirizzo (address) dello studente (file app/Student.php).

class Student extends Model
{
    protected $table = "students";

    public function getAttribute($key) {
        //Prelevo l'attributo
        $value = parent::getAttribute($key);

        if ($key === 'address') {
            //Se stiamo prelevando l'indirizzo allora decifro il valore
            $value = decrypt($value);
        }

        return $value;
    }

    public function setAttribute($key, $value)
    {
        if ($key === 'address') {
            //Se stiamo settando l'indirizzo allora cifro il valore
            $value = encrypt($value);
        }

        //Setto l'attributo
        return parent::setAttribute($key, $value);
    }
}

In fase di prelievo di un attributo, il nostro model andrà a controllare se l’attributo in questione è “address”. In caso positivo ne decripterà il valore prima di restituirlo.
Analogamente in fase di assegnazione, il nostro model, se l’attributo è “address”, andrà a criptarne il valore.
Ovviamente l’invocazione dei metodi del parent è necessaria ad un coretto funzionamento del model (come già accennato getAttribute e setAttribute non si limitano a gestire l’array $attributes).

Gli “helper” encryptdecryp utilizzano l’application key settata nel file di environment. Per maggiori informazioni consulta la documentazione ufficiale.

 

Un trait per la gestione della crittografia

L’approccio visto fino ad ora (ovvero l’override di getAttribute e setAttribute), sebbene sia il modo migliore per intercettare la lettura e scrittura dei dati su database, per come è stato implementato risulta abbastanza rigido e poco riusabile. Vediamo quindi di isolarne le logiche in un apposito trait, avendo cura di introdurre un apposito array ($encryptable) per la gestione dei campi che dovranno essere criptati.
Aggiungiamo al nostro progetto il file app/Traits/Encryptable.php:

namespace App\Traits;

trait Encryptable {
    public function getAttribute($key) {
        $value = parent::getAttribute($key);
 
        if(isset($this->encryptable) && in_array($key, $this->encryptable) && (!is_null($value))) {   
            $value = unserialize(
                decrypt($value)
            );
        }
 
        return $value;
    }
 
    public function setAttribute($key, $value) {
        if(isset($this->encryptable) && in_array($key, $this->encryptable) && (!is_null($value))) {
            $value = encrypt(
                serialize($value)
            );
        }
 
        return parent::setAttribute($key, $value);
    }
}

Da notare come il metodo, prima di procedere ad un encrypt/decript, controlli l’effettiva esistenza dell’array $encryptable, oltre ad eseguire un check sul valore, per evitare di criptare un null.
Per consentire al trait di gestire anche i tipi di dati diversi da string, prima di criptare/decriptare, il valore assegnato all’attributo viene serializzato/deserializzato (non è sicuramente la soluzione più elegante, ma è di sicuro quella più veloce da implementare).

A questo punto, il nostro model potrà essere modificato per criptare tutti i campi:

class Student extends Model {
    use \App\Traits\Encryptable;

    protected $table = "students";

    protected $encryptable = [
        'name',
        'last_name',
        'birthday',
        'city',
        'address',
    ];
}

Facciamo una prova al volo, usando la tinker console:

Tinker console

Come potete notare, il nostro model Student, contiene i dati in forma criptata.

Tinker console

Gestire il cambio di chiave

A questo punto resta un’ultima miglioria, da apportare al nostro trait: la gestione del cambio chiave. Può essere utile sotto molti aspetti, avere a disposizione un metodo che ci consenta di cambiare chiave crittografica.
Modifichiamo quindi il file app/Traits/Encryptable.php:

namespace App\Traits;
 
use Illuminate\Support\Facades\Config;
use Illuminate\Contracts\Encryption\DecryptException;
 
trait Encryptable 
{
    [...]

    public function updateKey($encryptionKey) {
        $encrypter = new \Illuminate\Encryption\Encrypter(
            $encryptionKey, 
            Config::get('app.cipher')
        );

        if(isset($this->encryptable)) {
            foreach($this->encryptable as $key) {
                $value = $this->getAttribute($key);
                if($value != null) {
                    $encryptedValue = $newEncrypter->encrypt($value);
                    parent::setAttribute($key, $encryptedValue);
                }
            }
        }
    }
}

Il metodo, che riceve come parametro la nuova chiave crittografica, dopo aver istanziato un nuovo Encrypter, lo utilizza per convertire gli attributi presenti nell’array $encryptable.
Da notare che, in fase di prelievo dell’attributo, viene invocato il metodo getAttribute del trait (che ritorna il valore in chiaro), mentre per memorizzare il valore viene invece invocato setAttribute del parent (per evitare di criptare 2 volte il valore).

Considerazioni sullo spazio occupato

Il metodo usato per criptare i dati, genera un testo in base64. Questo si traduce in un considerevole consumo di spazio. Per fare un esempio, un testo in chiaro di 200 caratteri, in forma criptata, supera abbondantemente i 500 caratteri. Serializzare i valori con serialize di certo non aiuta a risparmiare spazio. È necessario quindi tenere in considerazione questo fattore in fase di design del database (nel quale sarà necessario prevedere dei campi criptati di tipo text o longText a seconda delle necessità).

È altresì chiaro che, in fase di utilizzo, sarebbe il caso (come sempre del resto) di limitare il prelievo di dati (una select limitata ai soli campi strettamente necessari), per evitare inutili cali di performance e/o spreco di memoria.

Conclusioni

Abbiamo visto come sviluppare un trait in grado di criptare gli attributi dei nostri model e abbiamo visto come è possibile eseguire un cambio password.

Sebbene il metodo sia semplice e di facile implementazione non è esente da problematiche: se da un lato potremmo dire che questo tipo di decriptazione dei dati, alla bisogna, ci fornisce una maggiore sicurezza (i dati vengono mantenuti in memoria in forma criptata e decriptati solo quando necessari) dall’altro (sebbene eloquent non necessiti di decriptare l’intera entità, per manipolarne il contenuto) ogni volta che andremo a scrivere o leggere il valore di un attributo, eloquent eseguirà il crypt/decrypt del dato.

Una scrittura di questo tipo:

foreach($addresses as $address) {
    $student->address .= $address;
}

si traduce quindi in una serie dispendiosa di crypt e decrypt che può facilmente deteriorare le performance della nostra applicazione.

A proposito di me

Paolo Rizzello

Nato e cresciuto in un'azienda di informatica, ha (inevitabilmente) intrapreso la strada dell'IT con un diploma da perito informatico prima e una laurea in ingegneria informatica poi, facendo, della sua passione, un lavoro a tutti gli effetti.
Si guadagna da vivere sviluppando web application in Laravel e ASP.NET... ma ha un oscuro passato di sviluppatore C++.

Di Paolo Rizzello

Gli articoli più letti

Articoli recenti

Commenti recenti