Recentemente ho partecipato a Laracon EU 2018 dove Marcus Bointon ha tenuto un grande discorso su Crypto in PHP 7.2. Ho lasciato il discorso con un apprezzamento molto maggiore per quanto sia complicata la crittografia, ma anche per come PHP stia rendendo la crittografia più accessibile grazie all’introduzione di Sodium. La crittografia dei dati in PHP è qualcosa su cui ho lavorato come parte del mio lavoro su SpinupWP, quindi ho pensato che fosse giunto il momento di condividere alcune intuizioni. Allacciate le cinture, perché questo potrebbe essere un giro accidentato!
Tipi di crittografia
Oggi esistono diversi metodi di crittografia, il più comune è l’hashing, la crittografia a chiave segreta e la crittografia a chiave pubblica. Inoltre, ogni metodo di crittografia ha più algoritmi o cifrari tra cui scegliere (ognuno con i propri punti di forza e di debolezza). In questo articolo ci concentreremo sull’implementazione dell’hashing e della crittografia a chiave segreta.
Hashing
Un algoritmo di hashing prende un valore di input e lo trasforma in un digest di messaggi. In poche parole, i valori di testo in chiaro vengono trasformati in un hash di lunghezza fissa e possono essere convalidati solo passando il valore originale all’algoritmo di hashing. Questo rende l’hashing perfetto per memorizzare le password degli utenti.
Vale la pena notare che l’hashing non è una soluzione antiproiettile e non tutti gli algoritmi di hashing sono uguali. Considera MD5 e SHA1 che sono veloci ed efficienti, rendendoli ideali per il checksumming e la verifica dei file. Tuttavia, la loro velocità li rende inadatti per l’hashing della password di un utente. Con l’odierna potenza computazionale delle GPU moderne, una password può essere decifrata con la forza bruta in pochi minuti, rivelando la password originale in chiaro. Invece, dovrebbero essere usati algoritmi di hashing intenzionalmente più lenti come bcrypt o Argon2.
Mentre una password hash generata da qualsiasi algoritmo oscurerà certamente i dati originali e rallenterà qualsiasi aspirante attaccante, noi come sviluppatori dovremmo sforzarci di utilizzare l’algoritmo più forte disponibile. Fortunatamente, PHP rende questo facile grazie a password_hash()
.
$hash = password_hash($password, PASSWORD_DEFAULT);
La funzione password_hash()
non solo utilizza un algoritmo di hashing unidirezionale sicuro, ma gestisce automaticamente il sale e previene gli attacchi a canale laterale basati sul tempo. A partire da PHP 5.5, bcrypt verrà utilizzato per generare l’hash, ma questo cambierà in futuro con l’aggiunta di algoritmi di hashing più recenti e più sicuri a PHP. È probabile che Argon2 diventi il prossimo algoritmo di hashing predefinito e possa essere utilizzato oggi (su PHP 7.2) passando il flag PASSWORD_ARGON2I
invece di PASSWORD_DEFAULT
.
Verificare la password di un utente è anche un processo banale grazie alla funzione password_verify()
. Basta passare la password in chiaro fornita dall’utente e confrontarla con l’hash memorizzato, in questo modo:
if (password_verify($password, $hash)) { echo "Let me in, I'm genuine!";}
Si noti come viene eseguita la verifica della password in PHP. Se stai memorizzando le credenziali di un utente in un database, potresti essere incline a cancellare la password inserita al login e quindi eseguire una query di database, in questo modo:
SELECT * FROM usersWHERE username = 'Ashley'AND password = 'password_hash'LIMIT 1;
Questo approccio è suscettibile di attacchi a canale laterale e dovrebbe essere evitato. Invece, restituire l’utente e quindi controllare l’hash della password in PHP.
SELECT username, password FROM usersWHERE username = 'Ashley'LIMIT 1;
Mentre l’hashing è ottimo per memorizzare la password di un utente, non funziona per dati arbitrari a cui la nostra applicazione deve accedere senza l’intervento dell’utente. Consideriamo un’applicazione di fatturazione, che crittografa i dati della carta di credito di un utente. Ogni mese la nostra applicazione deve fatturare all’utente per l’utilizzo del mese precedente. L’hashing dei dati della carta di credito non funzionerà perché richiede che la nostra applicazione conosca i dati originali per recuperarli in chiaro.
Crittografia a chiave segreta in soccorso!
Crittografia a chiave segreta
La crittografia a chiave segreta (o crittografia simmetrica come è anche conosciuta) utilizza una singola chiave per crittografare e decrittografare i dati. Vediamo come implementeremmo un tale meccanismo usando Sodium, che è stato introdotto in PHP 7.2. Se si sta eseguendo una versione precedente di PHP è possibile installare sodium tramite PECL.
Per prima cosa abbiamo bisogno di una chiave di crittografia, che può essere generata utilizzando la funzione random_bytes()
. Di solito, lo farai solo una volta e lo memorizzerai come variabile di ambiente. Ricorda che questa chiave deve essere tenuta segreta a tutti i costi. Una volta che la chiave è compromessa, così è tutti i dati crittografati.
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
Per crittografare il valore lo passiamo a sodium_crypto_secretbox()
con il nostro $key
e un $nonce
. Il nonce viene generato usando random_bytes()
, perché lo stesso nonce non dovrebbe mai essere riutilizzato.
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);$ciphertext = sodium_crypto_secretbox('This is a secret!', $nonce, $key);
Questo presenta un problema perché abbiamo bisogno del nonce per decifrare il valore in seguito. Fortunatamente, i nonces non devono essere tenuti segreti in modo da poterlo anteporre al nostro $ciphertext
quindi base64_encode()
il valore prima di salvarlo nel database.
$encoded = base64_encode($nonce . $ciphertext);var_dump($encoded);// string 'v6KhzRACVfUCyJKCGQF4VNoPXYfeFY+/pyRZcixz4x/0jLJOo+RbeGBTiZudMLEO7aRvg44HRecC' (length=76)
Quando si tratta di decifrare il valore, facciamo il contrario.
$decoded = base64_decode($encoded);
Poiché conosciamo la lunghezza di nonce possiamo estrarla usando mb_substr()
prima di decifrare il valore.
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);var_dump($plaintext);// string 'This is a secret!' (length=17)
Questo è tutto quello che c’è da crittografia a chiave segreta in PHP, grazie a Sodium!
Crittografia della busta
Mentre l’approccio sopra descritto è certamente un passo nella giusta direzione, lascia ancora i nostri dati vulnerabili se la chiave segreta è compromessa. Consideriamo un utente malintenzionato che ottiene l’accesso al server che ospita la nostra applicazione. In questo scenario, è probabile che l’attaccante sarà in grado di scoprire la nostra chiave segreta che abbiamo usato per crittografare i dati. Questo lascia i nostri dati completamente esposti.
La soluzione semplice è non memorizzare la nostra chiave segreta nella stessa posizione dei dati crittografati, ma questo presenta un problema. Come crittografare e decrittografare su richiesta? Inserisci il servizio di gestione delle chiavi di Google Cloud (Cloud KMS).
Cloud KMS è un servizio fornito da Google per l’hosting sicuro di chiavi crittografiche. Esso fornisce una varietà di funzioni utili intorno stoccaggio chiave, tra cui la rotazione automatica dei tasti e la distruzione chiave ritardata. Tuttavia, in questo esempio ci occupiamo principalmente di memorizzare la nostra chiave segreta lontano dai nostri dati.
Per rendere le cose più sicure useremo una tecnica nota come crittografia della busta. Essenzialmente, la crittografia della busta comporta la crittografia delle chiavi con un’altra chiave. Lo facciamo per due motivi:
- Cloud KMS ha un limite di dimensioni di 64 KiB sui dati che possono essere crittografati e decifrati. Pertanto, potrebbe non essere possibile inviare tutti i dati in un colpo solo.
- Ancora più importante non vogliamo inviare i nostri dati sensibili in chiaro a terzi, indipendentemente da quanto possano sembrare affidabili.
Invece di inviare i nostri dati in chiaro a Cloud KMS, genereremo una chiave di crittografia univoca ogni volta che scriviamo dati sensibili nel database. Questa chiave è nota come chiave di crittografia dei dati (DEK), che verrà utilizzata per crittografare i nostri dati. Il DEK viene quindi inviato a Cloud KMS per essere crittografato, che restituisce una chiave di crittografia a chiave (nota come KEK). Infine, il KEK viene memorizzato fianco a fianco nel database accanto ai dati crittografati e il DEK viene distrutto. Il processo sembra così:
- Generare una chiave di crittografia univoca (DEK)
- Crittografare i dati utilizzando la chiave segreta di crittografia
- Invia l’unica chiave di crittografia (DEK) per il Cloud KM per la crittografia, che restituisce il KEK
- Archivio crittografato di dati e di una chiave di cifratura (KEK) side-by-side
- Distruggere la chiave generata (DEK)
Quando decrittografia dei dati il processo è invertito:
- Recuperare i dati crittografati e di una chiave di cifratura (KEK) dal database
- Invia il KEK Cloud KM per la decriptazione, che restituisce il DEK
- Utilizzare il DEK per decrittografare i nostri dati crittografati
- Distruggere il DEK
Con questo in mente, ho creato una semplice classe di supporto per l’esecuzione di busta di crittografia. Non ho intenzione di andare oltre i passaggi richiesti nella console Google Cloud, come il Quickstart e l’autenticazione guide delineare tutto il necessario per iniziare. Per brevità non c’è gestione degli errori, ecc. in questo esempio.
<?phpuse Google_Service_CloudKMS as Kms;use Google_Service_CloudKMS_DecryptRequest as DecryptRequest;use Google_Service_CloudKMS_EncryptRequest as EncryptRequest;class KeyManager{ private $kms; private $encryptRequest; private $decryptRequest; private $projectId; private $locationId; private $keyRingId; private $cryptoKeyId; public function __construct(Kms $kms, EncryptRequest $encryptRequest, DecryptRequest $decryptRequest, $projectId, $locationId, $keyRingId, $cryptoKeyId) { $this->kms = $kms; $this->encryptRequest = $encryptRequest; $this->decryptRequest = $decryptRequest; $this->projectId = $projectId; $this->locationId = $locationId; $this->keyRingId = $keyRingId; $this->cryptoKeyId = $cryptoKeyId; } public function encrypt($data) { $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = sodium_crypto_secretbox($data, $nonce, $key); return ; } public function decrypt($secret, $data) { $decoded = base64_decode($data); $key = $this->decryptSecret($secret); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); return sodium_crypto_secretbox_open($ciphertext, $nonce, $key); } private function encryptKey($key) { $this->encryptRequest->setPlaintext(base64_encode($key)); $response = $this->kms->projects_locations_keyRings_cryptoKeys->encrypt( $this->getResourceName(), $this->encryptRequest ); return $response; } private function decryptSecret($secret) { $this->decryptRequest->setCiphertext($secret); $response = $this->kms->projects_locations_keyRings_cryptoKeys->decrypt( $this->getResourceName(), $this->decryptRequest ); return base64_decode($response); } private function getResourceName() { return sprintf( 'projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s', $this->projectId, $this->locationId, $this->keyRingId, $this->cryptoKeyId ); }}
Noterai che i metodi di crittografia e decrittografia effettivi sono quasi identici all’implementazione della chiave segreta introdotta sopra. La differenza tuttavia è che ora stiamo usando più chiavi di crittografia. Vediamo la classe helper in azione. Dovrai fornire il tuo $projectId
, $locationId
, $keyRingId
e $cryptoKeyId
che sono disponibili dalla console Google Cloud.
<?phpuse Google_Service_CloudKMS as Kms;use Google_Service_CloudKMS_DecryptRequest as DecryptRequest;use Google_Service_CloudKMS_EncryptRequest as EncryptRequest;$client = new Google_Client();$client->setAuthConfig(getenv('GOOGLE_CREDENTIALS_FILE'));$client->addScope('https://www.googleapis.com/auth/cloud-platform');$keyManager = new KeyManager( new Kms($client), new EncryptRequest(), new DecryptRequest(), $projectId, $locationId, $keyRingId, $cryptoKeyId);$encrypted = $keyManager->encrypt('This is a secret!');var_dump($encrypted);// array (size=2)// 'data' => string 'uKjmEU7e1JEU+2vL3hBK2wBk6afCSgb+Y4GQtu/mmLuffgHlnqxnqOMPOI6WGkM18vAGGvFVDTvd' (length=76)// 'secret' => string 'CiQAdA0emUW2nhlU3RijX/5GnUsTnPPrQdLZNxdHWXWYugx49a4SSQBHyYr0T/PEbKwyFhIkaZl28oKkJRkXqNcqOL4Z+OTQFLpGvS6zCDt2mFn/nUQ/bi4znD4DORk9ZDTqiIBK3UNFUZcrXvoExds=' (length=152)$decrypted = $keyManager->decrypt($encrypted, $encrypted);var_dump($decrypted);// string 'This is a secret!' (length=17)
Se un utente malintenzionato ha compromesso il nostro sistema, sarebbe in grado di ottenere anche le nostre credenziali API per Cloud KMS? A seconda del metodo di autenticazione utilizzato, allora sì potrebbe essere una possibilità. Se questo è il caso, ci si potrebbe chiedere come la crittografia busta è più sicuro di normale crittografia a chiave segreta? La differenza fondamentale (gioco di parole) è che l’accesso alle API può essere revocato, ostacolando così un utente malintenzionato che è stato eliminato con i tuoi dati sensibili. È l’equivalente di cambiare le serrature se qualcuno ruba la chiave di casa. Con la normale crittografia a chiave segreta in cui una singola chiave locale è compromessa, non hai quel lusso. L’attaccante ha tutto il tempo del mondo per decifrare i dati sensibili.
Concludere
La sicurezza e la crittografia dei dati sono argomenti vasti e ho coperto solo una manciata di modi per proteggere i dati sensibili usando PHP. (In precedenza abbiamo scritto sulla protezione di questo tipo di dati nel tuo ambiente locale)