Шифрование -- это обратимое преобразование данных для обеспечения конфиденциальности. В отличие от хеширования, зашифрованные данные можно расшифровать обратно при наличии ключа. PHP предоставляет два основных расширения: OpenSSL (широкий набор алгоритмов) и Sodium (современный, безопасный по умолчанию).
Симметричное vs асимметричное шифрование
Симметричное шифрование:
Один ключ для шифрования и расшифровки
[Данные] --[ключ]--> [Шифр] --[тот же ключ]--> [Данные]
Примеры: AES-256-CBC, AES-256-GCM, XSalsa20-Poly1305
Асимметричное шифрование:
Пара ключей: публичный (шифрует) + приватный (расшифровывает)
[Данные] --[публичный ключ]--> [Шифр] --[приватный ключ]--> [Данные]
Примеры: RSA, X25519, Ed25519
Гибридное шифрование (на практике):
1. Генерируем случайный симметричный ключ (session key)
2. Шифруем данные симметричным ключом (быстро)
3. Шифруем симметричный ключ публичным ключом получателя
4. Отправляем: зашифрованные данные + зашифрованный ключ
Генерация случайных данных (CSPRNG)
<?php
declare(strict_types=1);
// random_bytes() — cryptographically secure random bytes (PHP 7.0+)
$key = random_bytes(32); // 256-bit key
$iv = random_bytes(16); // 128-bit IV
$nonce = random_bytes(24); // 192-bit nonce
// random_int() — cryptographically secure random integer
$otp = random_int(100000, 999999); // 6-digit OTP code
// Random\Randomizer class (PHP 8.2+) — OOP interface
$randomizer = new Random\Randomizer(new Random\Engine\Secure());
$bytes = $randomizer->getBytes(32);
$int = $randomizer->getInt(1, 100);
$shuffled = $randomizer->shuffleString('abcdefgh');
// Generate a random string from specific characters
$randomizer = new Random\Randomizer();
$token = $randomizer->getBytesFromString(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
32
); // PHP 8.3+
// ❌ NEVER use for cryptography:
// rand(), mt_rand(), array_rand(), uniqid(), microtime()
// These are NOT cryptographically secure!
OpenSSL -- симметричное шифрование
AES-256-CBC
<?php
declare(strict_types=1);
// AES-256-CBC — classic symmetric encryption
// CBC mode requires IV and padding, does NOT provide authentication
$plaintext = 'Sensitive user data';
$key = random_bytes(32); // 256-bit key for AES-256
// Get required IV length for the cipher
$ivLength = openssl_cipher_iv_length('aes-256-cbc');
$iv = openssl_random_pseudo_bytes($ivLength); // 16 bytes for AES
// Encrypt
$ciphertext = openssl_encrypt(
data: $plaintext,
cipher_algo: 'aes-256-cbc',
passphrase: $key,
options: OPENSSL_RAW_DATA, // Return raw bytes, not base64
iv: $iv,
);
// Decrypt
$decrypted = openssl_decrypt(
data: $ciphertext,
cipher_algo: 'aes-256-cbc',
passphrase: $key,
options: OPENSSL_RAW_DATA,
iv: $iv,
);
echo $decrypted; // 'Sensitive user data'
// ⚠️ CBC mode is NOT authenticated — use GCM instead!
// An attacker can modify ciphertext without detection (padding oracle attack)
// For storage/transmission: combine IV + ciphertext
$stored = base64_encode($iv . $ciphertext);
// For retrieval: split IV and ciphertext
$decoded = base64_decode($stored);
$iv = substr($decoded, 0, $ivLength);
$ciphertext = substr($decoded, $ivLength);
AES-256-GCM (рекомендуется)
<?php
declare(strict_types=1);
// AES-256-GCM — Authenticated Encryption with Associated Data (AEAD)
// Provides both confidentiality AND integrity/authenticity
$plaintext = 'Credit card: 4111-1111-1111-1111';
$key = random_bytes(32);
$ivLength = openssl_cipher_iv_length('aes-256-gcm');
$iv = random_bytes($ivLength); // 12 bytes for GCM
// Additional Authenticated Data — integrity-protected but not encrypted
$aad = 'user_id=42;context=payment';
// Encrypt — $tag will be populated with authentication tag
$tag = '';
$ciphertext = openssl_encrypt(
data: $plaintext,
cipher_algo: 'aes-256-gcm',
passphrase: $key,
options: OPENSSL_RAW_DATA,
iv: $iv,
tag: $tag, // Output: authentication tag (16 bytes by default)
aad: $aad,
tag_length: 16, // Tag length in bytes
);
// Decrypt — must provide the same IV, tag, and AAD
$decrypted = openssl_decrypt(
data: $ciphertext,
cipher_algo: 'aes-256-gcm',
passphrase: $key,
options: OPENSSL_RAW_DATA,
iv: $iv,
tag: $tag,
aad: $aad,
);
if ($decrypted === false) {
throw new RuntimeException('Decryption failed — data tampered or wrong key');
}
echo $decrypted; // 'Credit card: 4111-1111-1111-1111'
// For storage: IV + tag + ciphertext
function encryptAesGcm(string $plaintext, string $key, string $aad = ''): string
{
$iv = random_bytes(12);
$tag = '';
$ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, $aad, 16);
if ($ciphertext === false) {
throw new RuntimeException('Encryption failed: ' . openssl_error_string());
}
// Format: IV (12) + Tag (16) + Ciphertext
return $iv . $tag . $ciphertext;
}
function decryptAesGcm(string $encrypted, string $key, string $aad = ''): string
{
$iv = substr($encrypted, 0, 12);
$tag = substr($encrypted, 12, 16);
$ciphertext = substr($encrypted, 28);
$plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, $aad);
if ($plaintext === false) {
throw new RuntimeException('Decryption failed — data corrupted or wrong key');
}
return $plaintext;
}
// Available ciphers
$ciphers = openssl_get_cipher_methods();
// Filter only unique (remove uppercase duplicates)
$ciphers = array_filter($ciphers, fn(string $c) => $c === strtolower($c));
OpenSSL -- асимметричное шифрование (RSA)
<?php
declare(strict_types=1);
// Generate RSA key pair
$config = [
'private_key_bits' => 4096, // Key size (minimum 2048!)
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$keyPair = openssl_pkey_new($config);
// Extract private key as PEM string
openssl_pkey_export($keyPair, $privateKeyPem);
echo $privateKeyPem;
// Extract public key
$details = openssl_pkey_get_details($keyPair);
$publicKeyPem = $details['key'];
// Save keys to files
file_put_contents('/secure/path/private.pem', $privateKeyPem);
file_put_contents('/secure/path/public.pem', $publicKeyPem);
// With passphrase protection for private key
openssl_pkey_export($keyPair, $protectedPrivateKey, 'my-passphrase', [
'encrypt_key_cipher' => OPENSSL_CIPHER_AES_256_CBC,
]);
// Encrypt with public key (anyone can encrypt)
$plaintext = 'Secret message';
openssl_public_encrypt($plaintext, $encrypted, $publicKeyPem);
// Decrypt with private key (only owner can decrypt)
openssl_private_decrypt($encrypted, $decrypted, $privateKeyPem);
echo $decrypted; // 'Secret message'
// ⚠️ RSA can only encrypt data smaller than key size minus padding
// For 4096-bit key with OAEP padding: max ~446 bytes
// For larger data, use hybrid encryption!
// Encrypt with private key (for signing, not confidentiality)
openssl_private_encrypt($plaintext, $signed, $privateKeyPem);
openssl_public_decrypt($signed, $verified, $publicKeyPem);
// Load existing keys from files
$privateKey = openssl_pkey_get_private(
file_get_contents('/secure/path/private.pem'),
'passphrase-if-protected'
);
$publicKey = openssl_pkey_get_public(file_get_contents('/secure/path/public.pem'));
OpenSSL -- цифровые подписи
<?php
declare(strict_types=1);
// Digital signatures: prove authenticity and integrity
// Sign with private key, verify with public key
$data = 'Document content to be signed';
// Generate key pair
$keyPair = openssl_pkey_new(['private_key_bits' => 2048]);
openssl_pkey_export($keyPair, $privateKey);
$publicKey = openssl_pkey_get_details($keyPair)['key'];
// Sign data
openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
// Verify signature (returns 1 = valid, 0 = invalid, false = error)
$result = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
match ($result) {
1 => echo "Signature VALID\n",
0 => echo "Signature INVALID\n",
false => echo "Error: " . openssl_error_string() . "\n",
};
// Sign a file
$fileData = file_get_contents('/path/to/document.pdf');
openssl_sign($fileData, $fileSignature, $privateKey, OPENSSL_ALGO_SHA256);
$signatureBase64 = base64_encode($fileSignature);
// Save/transmit $signatureBase64 alongside the file
OpenSSL -- сертификаты
<?php
declare(strict_types=1);
// Create a Certificate Signing Request (CSR)
$dn = [
'countryName' => 'US',
'stateOrProvinceName' => 'California',
'localityName' => 'San Francisco',
'organizationName' => 'My Company',
'organizationalUnitName' => 'Engineering',
'commonName' => 'example.com',
'emailAddress' => '[email protected]',
];
$privateKey = openssl_pkey_new(['private_key_bits' => 2048]);
$csr = openssl_csr_new($dn, $privateKey);
// Export CSR to PEM
openssl_csr_export($csr, $csrPem);
// Self-sign certificate (for development)
$cert = openssl_csr_sign($csr, null, $privateKey, 365);
openssl_x509_export($cert, $certPem);
// Parse certificate
$certInfo = openssl_x509_parse($certPem);
echo $certInfo['subject']['CN']; // 'example.com'
echo $certInfo['validFrom_time_t']; // Unix timestamp
echo $certInfo['validTo_time_t']; // Unix timestamp
Sodium -- симметричное шифрование (рекомендуется)
Sodium (libsodium) -- это современная криптографическая библиотека, встроенная в PHP с версии 7.2. Она спроектирована так, чтобы было сложно допустить ошибку.
<?php
declare(strict_types=1);
// === Symmetric encryption: XSalsa20-Poly1305 (secretbox) ===
$message = 'Top secret message';
// Generate key (32 bytes)
$key = sodium_crypto_secretbox_keygen();
// Or: $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); // 32
// Generate nonce (24 bytes) — must be unique per message!
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24
// Encrypt (automatically authenticated!)
$ciphertext = sodium_crypto_secretbox($message, $nonce, $key);
// Decrypt
$decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
if ($decrypted === false) {
throw new RuntimeException('Decryption failed — tampered or wrong key');
}
echo $decrypted; // 'Top secret message'
// ⚠️ ALWAYS wipe sensitive data from memory!
sodium_memzero($key);
sodium_memzero($decrypted);
// Helper for storage: nonce + ciphertext
function sodiumEncrypt(string $plaintext, string $key): string
{
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
// Prepend nonce to ciphertext
return $nonce . $ciphertext;
}
function sodiumDecrypt(string $encrypted, string $key): string
{
$nonceSize = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
$nonce = substr($encrypted, 0, $nonceSize);
$ciphertext = substr($encrypted, $nonceSize);
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
if ($plaintext === false) {
throw new RuntimeException('Decryption failed');
}
return $plaintext;
}
AEAD -- XChaCha20-Poly1305
<?php
declare(strict_types=1);
// AEAD — Authenticated Encryption with Associated Data
// XChaCha20-Poly1305-IETF — recommended for new projects
$message = 'Payment data';
$additionalData = 'user_id:42'; // Authenticated but not encrypted
$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); // 24 bytes
// Encrypt with AAD
$ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
$message,
$additionalData,
$nonce,
$key,
);
// Decrypt with AAD (must match exactly)
$decrypted = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$ciphertext,
$additionalData,
$nonce,
$key,
);
if ($decrypted === false) {
throw new RuntimeException('Decryption failed — AAD mismatch, tampered, or wrong key');
}
sodium_memzero($key);
Sodium -- асимметричное шифрование
<?php
declare(strict_types=1);
// === Asymmetric encryption: X25519-XSalsa20-Poly1305 (crypto_box) ===
// Generate key pairs for Alice and Bob
$aliceKeypair = sodium_crypto_box_keypair();
$alicePublicKey = sodium_crypto_box_publickey($aliceKeypair);
$aliceSecretKey = sodium_crypto_box_secretkey($aliceKeypair);
$bobKeypair = sodium_crypto_box_keypair();
$bobPublicKey = sodium_crypto_box_publickey($bobKeypair);
$bobSecretKey = sodium_crypto_box_secretkey($bobKeypair);
// Alice sends message to Bob
$message = 'Hello Bob, this is Alice!';
$nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES); // 24 bytes
// Create combined keypair: Alice's secret + Bob's public
$encryptionKeypair = sodium_crypto_box_keypair_from_secretkey_and_publickey(
$aliceSecretKey,
$bobPublicKey,
);
$ciphertext = sodium_crypto_box($message, $nonce, $encryptionKeypair);
// Bob decrypts using: Bob's secret + Alice's public
$decryptionKeypair = sodium_crypto_box_keypair_from_secretkey_and_publickey(
$bobSecretKey,
$alicePublicKey,
);
$decrypted = sodium_crypto_box_open($ciphertext, $nonce, $decryptionKeypair);
echo $decrypted; // 'Hello Bob, this is Alice!'
// Anonymous encryption (sealed box) — sender is anonymous
// Only recipient's public key is needed
$sealed = sodium_crypto_box_seal($message, $bobPublicKey);
// Bob decrypts with his full keypair
$opened = sodium_crypto_box_seal_open($sealed, $bobKeypair);
echo $opened; // 'Hello Bob, this is Alice!'
// Clean up sensitive keys
sodium_memzero($aliceSecretKey);
sodium_memzero($bobSecretKey);
Sodium -- цифровые подписи (Ed25519)
<?php
declare(strict_types=1);
// Ed25519 signatures — fast and secure
// Generate signing key pair
$signingKeypair = sodium_crypto_sign_keypair();
$signingPublicKey = sodium_crypto_sign_publickey($signingKeypair);
$signingSecretKey = sodium_crypto_sign_secretkey($signingKeypair);
$message = 'This document is authentic';
// Sign (combined mode — signature prepended to message)
$signedMessage = sodium_crypto_sign($message, $signingSecretKey);
// Verify and extract original message
$originalMessage = sodium_crypto_sign_open($signedMessage, $signingPublicKey);
if ($originalMessage === false) {
throw new RuntimeException('Invalid signature!');
}
echo $originalMessage; // 'This document is authentic'
// Detached signature (signature separate from message)
$signature = sodium_crypto_sign_detached($message, $signingSecretKey);
// Verify detached signature
$valid = sodium_crypto_sign_verify_detached($signature, $message, $signingPublicKey);
if (!$valid) {
throw new RuntimeException('Signature verification failed!');
}
// Key conversion: Ed25519 → X25519 (signing → encryption)
$encryptionPublicKey = sodium_crypto_sign_ed25519_pk_to_curve25519($signingPublicKey);
$encryptionSecretKey = sodium_crypto_sign_ed25519_sk_to_curve25519($signingSecretKey);
sodium_memzero($signingSecretKey);
Sodium -- безопасная работа с памятью
<?php
declare(strict_types=1);
// Wipe sensitive data from memory
$key = sodium_crypto_secretbox_keygen();
// ... use the key ...
sodium_memzero($key); // Overwrite memory with zeros
// $key is now empty string
// Safe encoding/decoding (no side-channel leaks)
$binary = random_bytes(32);
// Constant-time hex encoding (safe against timing attacks)
$hex = sodium_bin2hex($binary);
$back = sodium_hex2bin($hex);
// Constant-time base64 encoding
$b64 = sodium_bin2base64($binary, SODIUM_BASE64_VARIANT_ORIGINAL);
$back = sodium_base64_todec($b64, SODIUM_BASE64_VARIANT_ORIGINAL);
// URL-safe base64
$urlSafe = sodium_bin2base64($binary, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
// Constant-time comparison
$equal = sodium_compare($str1, $str2); // Returns -1, 0, or 1
// For equality: sodium_memcmp($a, $b) returns 0 if equal
// Increment a nonce (for counter-based nonces)
$nonce = random_bytes(24);
sodium_increment($nonce); // Safely increments as big-endian number
Sodium -- хеширование паролей (Argon2)
<?php
declare(strict_types=1);
// Sodium provides its own password hashing API
$password = 'user-password-here';
// Hash password with Argon2id
$hash = sodium_crypto_pwhash_str(
$password,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, // CPU cost
SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, // Memory cost (64 MB)
);
// Verify password
if (sodium_crypto_pwhash_str_verify($hash, $password)) {
echo "Password valid!\n";
}
// Check if rehash needed
if (sodium_crypto_pwhash_str_needs_rehash(
$hash,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE,
)) {
// Rehash with stronger parameters
$newHash = sodium_crypto_pwhash_str(
$password,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE,
);
}
// Derive encryption key from password
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES); // 16 bytes
$key = sodium_crypto_pwhash(
SODIUM_CRYPTO_SECRETBOX_KEYBYTES, // Output length (32)
$password,
$salt,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13,
);
sodium_memzero($password);
Практический пример: шифрование пользовательских данных
<?php
declare(strict_types=1);
final class DataEncryptor
{
private string $key;
public function __construct(string $hexKey)
{
$this->key = sodium_hex2bin($hexKey);
if (strlen($this->key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new InvalidArgumentException('Key must be 32 bytes');
}
}
public function encrypt(string $plaintext): string
{
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->key);
return sodium_bin2base64(
$nonce . $ciphertext,
SODIUM_BASE64_VARIANT_ORIGINAL,
);
}
public function decrypt(string $encoded): string
{
$decoded = sodium_base2bin(
$encoded,
SODIUM_BASE64_VARIANT_ORIGINAL,
);
$nonceSize = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
$nonce = substr($decoded, 0, $nonceSize);
$ciphertext = substr($decoded, $nonceSize);
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);
if ($plaintext === false) {
throw new RuntimeException('Decryption failed');
}
return $plaintext;
}
public function __destruct()
{
sodium_memzero($this->key);
}
}
// Usage
$key = sodium_bin2hex(sodium_crypto_secretbox_keygen());
// Store $key in environment variable, NOT in code!
$encryptor = new DataEncryptor($key);
$encrypted = $encryptor->encrypt('SSN: 123-45-6789');
$decrypted = $encryptor->decrypt($encrypted);
Практический пример: шифрование файлов
<?php
declare(strict_types=1);
function encryptFile(string $inputPath, string $outputPath, string $key): void
{
$plaintext = file_get_contents($inputPath);
if ($plaintext === false) {
throw new RuntimeException("Cannot read file: $inputPath");
}
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
// Write nonce + ciphertext
file_put_contents($outputPath, $nonce . $ciphertext);
sodium_memzero($plaintext);
}
function decryptFile(string $inputPath, string $outputPath, string $key): void
{
$data = file_get_contents($inputPath);
if ($data === false) {
throw new RuntimeException("Cannot read file: $inputPath");
}
$nonceSize = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
$nonce = substr($data, 0, $nonceSize);
$ciphertext = substr($data, $nonceSize);
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
if ($plaintext === false) {
throw new RuntimeException('File decryption failed');
}
file_put_contents($outputPath, $plaintext);
sodium_memzero($plaintext);
}
// Usage
$key = sodium_crypto_secretbox_keygen();
encryptFile('/data/sensitive.csv', '/data/sensitive.csv.enc', $key);
decryptFile('/data/sensitive.csv.enc', '/data/sensitive.csv', $key);
sodium_memzero($key);
Best practices
1. Используйте Sodium вместо OpenSSL для новых проектов
2. ВСЕГДА используйте authenticated encryption (GCM, Poly1305)
3. НИКОГДА не используйте ECB режим — он не скрывает паттерны данных
4. IV/nonce должен быть УНИКАЛЬНЫМ для каждого сообщения
5. Храните ключи в env variables или key management service, НЕ в коде
6. Очищайте ключи из памяти после использования (sodium_memzero)
7. Используйте random_bytes() для генерации ключей и IV
8. RSA: минимум 2048 бит, рекомендуется 4096
9. Для паролей: НИКОГДА не шифруйте — только хешируйте (password_hash)
10. Регулярно ротируйте ключи шифрования