La crittografia è uno strumento pratico per proteggere dati sensibili (password, token, informazioni personali, segreti applicativi) e per garantire proprietà come riservatezza, integrità e autenticità. In PHP oggi hai due “famiglie” principali di API a disposizione: libsodium (raccomandata, moderna e più difficile da usare male) e OpenSSL (molto diffusa, potente, ma più “tagliente” e soggetta a errori se non si rispettano le regole su IV, padding, modalità e autenticazione).
In questo articolo vediamo cosa fare e cosa evitare, con esempi completi in PHP. L’obiettivo è uscire con una “cassetta degli attrezzi” sicura per i casi più comuni: password, cifratura simmetrica, token firmati, file cifrati, scambio di chiavi e gestione dei segreti.
Concetti essenziali
Hash vs cifratura
- Hash: funzione a senso unico. Dato un input produce un digest; non si può “decifrare” l’hash. Utile per password e integrità.
- Cifratura: trasformazione reversibile; serve una chiave per decifrare. Utile per proteggere dati che poi devi recuperare.
Integrità e autenticazione (non solo segretezza)
Cifrare senza autenticare è una trappola classica: se un attaccante modifica il ciphertext, potresti decifrare dati corrotti
senza accorgertene. Per questo si usa AEAD (Authenticated Encryption with Associated Data) come
ChaCha20-Poly1305 o AES-256-GCM. In breve: cifra + autentica nello stesso algoritmo.
Casualità crittografica
Non usare rand() o mt_rand() per chiavi, nonce, token o sale. In PHP usa
random_bytes() e random_int().
<?php
$token = bin2hex(random_bytes(32)); // 64 caratteri esadecimali, ~256 bit
echo $token;
Password: hashing corretto
Le password non vanno mai cifrate “per poterle recuperare”: vanno hashate con un algoritmo
adattivo resistente a brute-force (con salt automatico e parametri configurabili).
In PHP lo standard pratico è password_hash() + password_verify().
<?php
$password = $_POST['password'] ?? '';
$hash = password_hash($password, PASSWORD_DEFAULT); // oggi spesso bcrypt/argon2, dipende dalla build
// Salva $hash nel DB
// Login: verifica
$ok = password_verify($password, $hash);
// Aggiorna l'hash se cambiano i parametri/algoritmi consigliati
if ($ok && password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$hash = password_hash($password, PASSWORD_DEFAULT);
// aggiorna nel DB
}
Scelta algoritmo e parametri
Se hai Argon2 disponibile, puoi impostarlo esplicitamente (per esempio PASSWORD_ARGON2ID)
e regolare costi in base alle risorse del server.
<?php
$options = [
'memory_cost' => 1<<17, // 128 MiB
'time_cost' => 3,
'threads' => 2,
];
$hash = password_hash($password, PASSWORD_ARGON2ID, $options);
Evita sha1, md5 e anche hash('sha256', ...) per password: non sono algoritmi progettati
per resistere a GPU/ASIC, e senza costi elevati diventano vulnerabili a attacchi offline.
Token e integrità: HMAC
Se devi garantire che un messaggio non sia stato alterato (ma non ti interessa nasconderlo), usa un HMAC con una chiave segreta. È tipico per link firmati, webhook, API keys “con firma”, ecc.
<?php
$secretKey = hex2bin('b1f0...'); // 32 byte, esempio
$data = "user_id=123&exp=1700000000";
$mac = hash_hmac('sha256', $data, $secretKey, true);
$token = rtrim(strtr(base64_encode($data . "." . $mac), '+/', '-_'), '=');
// Verifica
$decoded = base64_decode(strtr($token, '-_', '+/'), true);
[$data2, $mac2] = explode('.', $decoded, 2);
$expected = hash_hmac('sha256', $data2, $secretKey, true);
if (!hash_equals($expected, $mac2)) {
throw new RuntimeException("Token non valido");
}
Nota l’uso di hash_equals() per confronti in tempo costante: evita leak temporali che possono aiutare
a indovinare la firma.
Cifratura moderna con libsodium
Se puoi scegliere, libsodium è spesso la scelta migliore: API ad alto livello, nonce e tag gestiti correttamente, primitive robuste e default ragionevoli. In PHP è disponibile tramite l’estensione Sodium (molte distribuzioni la includono di default).
Secretbox: cifratura simmetrica autenticata
sodium_crypto_secretbox() implementa una cifratura autenticata (basata su XSalsa20-Poly1305).
Richiede:
- una chiave di 32 byte
- un nonce di 24 byte, unico per ogni messaggio con la stessa chiave
<?php
// Genera e conserva la chiave in modo sicuro (una volta sola)
$key = sodium_crypto_secretbox_keygen(); // 32 byte
$message = "Dati super sensibili";
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($message, $nonce, $key);
// Trasporto/storage: salva nonce + ciphertext insieme (nonce non è segreto)
$payload = base64_encode($nonce . $ciphertext);
// Decifrazione
$raw = base64_decode($payload, true);
$nonce2 = substr($raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher2 = substr($raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$plaintext = sodium_crypto_secretbox_open($cipher2, $nonce2, $key);
if ($plaintext === false) {
throw new RuntimeException("Autenticazione fallita o dati corrotti");
}
echo $plaintext;
AEAD con ChaCha20-Poly1305 e dati associati (AAD)
Con AEAD puoi autenticare anche metadati non cifrati (per esempio un ID record o un contesto). Quei metadati vengono verificati durante la decifrazione: se cambiano, la decifrazione fallisce.
<?php
$key = random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES);
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES);
$aad = "record_id=42";
$plaintext = json_encode(["email" => "mario@example.com", "role" => "admin"], JSON_THROW_ON_ERROR);
$cipher = sodium_crypto_aead_chacha20poly1305_ietf_encrypt(
$plaintext,
$aad,
$nonce,
$key
);
$payload = base64_encode($nonce . $cipher);
// Open
$raw = base64_decode($payload, true);
$nonce2 = substr($raw, 0, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES);
$cipher2 = substr($raw, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES);
$plain2 = sodium_crypto_aead_chacha20poly1305_ietf_decrypt(
$cipher2,
$aad, // deve combaciare
$nonce2,
$key
);
if ($plain2 === false) {
throw new RuntimeException("Decifrazione fallita (chiave/nonce/AAD errati o manomissione)");
}
Derivazione chiavi da password
Per cifrare dati con una password utente è rischioso usare direttamente la password come chiave.
Serve una funzione di derivazione (KDF) con parametri “costosi” e un salt. Con sodium puoi usare
sodium_crypto_pwhash() per ottenere una chiave robusta da una password.
<?php
$password = $_POST['passphrase'] ?? '';
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);
// Deriva 32 byte di chiave
$key = sodium_crypto_pwhash(
32,
$password,
$salt,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
// Usa $key con secretbox/AEAD.
// Salva anche $salt (non è segreto) insieme ai dati cifrati.
Se il caso d’uso è “archiviazione cifrata con password”, considera anche la gestione del cambio password: tipicamente si cifra una chiave dati casuale con una chiave derivata dalla password, così puoi ruotare la password senza ricifrare tutti i dati.
OpenSSL: AES-GCM fatto bene
OpenSSL è utile quando devi interoperare con altri sistemi o usare standard specifici. Se puoi, scegli AES-256-GCM (AEAD). Richiede:
- chiave da 32 byte (per AES-256)
- IV/nonce unico (tipicamente 12 byte per GCM)
- tag di autenticazione (di solito 16 byte)
<?php
$key = random_bytes(32); // conserva in modo sicuro
$iv = random_bytes(12); // 96 bit consigliati per GCM
$plaintext = "Contenuto riservato";
$aad = "context=user:123"; // opzionale
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv,
$tag,
$aad,
16
);
if ($ciphertext === false) {
throw new RuntimeException("Cifratura fallita");
}
// Salva/trasporta iv + tag + ciphertext
$payload = base64_encode($iv . $tag . $ciphertext);
// Decifrazione
$raw = base64_decode($payload, true);
$iv2 = substr($raw, 0, 12);
$tag2 = substr($raw, 12, 16);
$c2 = substr($raw, 28);
$plain2 = openssl_decrypt(
$c2,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv2,
$tag2,
$aad
);
if ($plain2 === false) {
throw new RuntimeException("Decifrazione fallita (manomissione o parametri errati)");
}
Errori comuni con OpenSSL
- Riutilizzare l’IV con la stessa chiave: per GCM è pericoloso. L’IV deve essere unico.
- Usare CBC senza MAC: AES-CBC senza autenticazione è vulnerabile (padding oracle e manomissione).
- Gestire male encoding: conserva i dati binari con base64 (o come BLOB) senza “tagliare” byte.
- Inventare formati senza versioning: inserisci un prefisso versione per aggiornare algoritmi/parametri.
Crittografia asimmetrica: firme e scambio chiavi
La crittografia asimmetrica usa una coppia di chiavi: una pubblica (distribuibile) e una privata (segreta). Serve tipicamente per:
- Firme digitali: garantire autenticità e integrità (chiunque verifica con la chiave pubblica).
- Scambio di chiavi: stabilire un segreto condiviso su canale insicuro.
Firme con libsodium (Ed25519)
Sodium rende le firme molto dirette. Puoi firmare un messaggio e verificarlo con la chiave pubblica.
<?php
$keypair = sodium_crypto_sign_keypair();
$publicKey = sodium_crypto_sign_publickey($keypair);
$secretKey = sodium_crypto_sign_secretkey($keypair);
$message = "Ordine: #123, importo=49.90";
$signature = sodium_crypto_sign_detached($message, $secretKey);
// Verifica
$ok = sodium_crypto_sign_verify_detached($signature, $message, $publicKey);
if (!$ok) {
throw new RuntimeException("Firma non valida");
}
Cifra con chiave pubblica (sealed boxes)
Se devi inviare un messaggio a un destinatario conoscendo solo la sua chiave pubblica, puoi usare
sodium_crypto_box_seal(). Il destinatario lo apre con la sua chiave privata.
<?php
$recipientKeypair = sodium_crypto_box_keypair();
$recipientPublic = sodium_crypto_box_publickey($recipientKeypair);
$recipientSecret = sodium_crypto_box_secretkey($recipientKeypair);
// Mittente: sigilla con la pubblica del destinatario
$sealed = sodium_crypto_box_seal("Messaggio per te", $recipientPublic);
// Destinatario: apre con la coppia di chiavi
$opened = sodium_crypto_box_seal_open($sealed, $recipientKeypair);
if ($opened === false) {
throw new RuntimeException("Impossibile aprire il messaggio");
}
Gestione delle chiavi: il vero punto critico
Anche l’algoritmo migliore fallisce se la chiave viene esposta o gestita male. Alcune regole pratiche:
- Non committare segreti: chiavi e password non devono finire in repository o immagini docker pubbliche.
- Usa variabili d’ambiente o secret manager: Vault, AWS Secrets Manager, GCP Secret Manager, ecc.
- Principio del minimo privilegio: l’app deve poter leggere solo i segreti necessari.
- Rotazione e versioning: conserva un identificatore di chiave (kid) per supportare più chiavi attive.
- Separazione: una chiave per scopo (password hashing ≠ cifratura dati ≠ HMAC).
Formato “versionato” per payload cifrati
Un trucco semplice per rendere il sistema aggiornabile è includere un prefisso versione e (se serve) un key id:
ad esempio v1:kid=2026-01:<base64>. In futuro potrai introdurre v2 con parametri diversi
senza rompere la compatibilità.
<?php
function packPayload(string $version, string $kid, string $binary): string {
return $version . ":kid=" . $kid . ":" . rtrim(strtr(base64_encode($binary), '+/', '-_'), '=');
}
function unpackPayload(string $payload): array {
$parts = explode(':', $payload, 3);
if (count($parts) !== 3) {
throw new InvalidArgumentException("Formato payload non valido");
}
[$version, $kidPart, $b64] = $parts;
if (!str_starts_with($kidPart, 'kid=')) {
throw new InvalidArgumentException("Kid mancante");
}
$kid = substr($kidPart, 4);
$bin = base64_decode(strtr($b64, '-_', '+/'), true);
if ($bin === false) {
throw new InvalidArgumentException("Base64 non valido");
}
return [$version, $kid, $bin];
}
Checklist rapida di sicurezza
- Per password:
password_hash/password_verify, mai SHA/MD5. - Per cifrare dati: usa AEAD (libsodium o AES-GCM), mai “solo cifratura” senza autenticazione.
- Nonce/IV: sempre unico per chiave; generato con
random_bytes. - Confronti: usa
hash_equalsper MAC e token. - Chiavi: non nel codice; usa secret manager; ruota e versiona.
- Encoding: conserva binario come base64 o BLOB; evita conversioni implicite.
- Test: crea test che verificano decifrazione, fallimenti su dati manomessi e compatibilità tra versioni.
Conclusione
In PHP puoi fare crittografia in modo robusto senza reinventare nulla: per le password usa l’API di hashing dedicata, per la cifratura preferisci libsodium e primitive AEAD, e con OpenSSL rimani su modalità autenticata come AES-GCM. Il passo decisivo, però, è trattare la gestione delle chiavi come parte del design: dove vengono conservate, come vengono ruotate, e come evolvono i formati dei dati nel tempo.