Шифрование и хеширование (Encryption & Hashing)
Шифрование и хеширование -- два фундаментально разных подхода к защите данных. Шифрование -- обратимый процесс (можно расшифровать). Хеширование -- необратимый процесс (нельзя восстановить оригинал). Laravel предоставляет удобные инструменты для обоих.
Шифрование (Encryption)
Laravel использует OpenSSL для шифрования AES-256-CBC или AES-256-GCM. Все зашифрованные значения подписаны HMAC для предотвращения модификации.
APP_KEY -- Ключ приложения
# Generate application key
php artisan key:generate
# The key is stored in .env
# APP_KEY=base64:wOHvE2f75r...
Критически важно:
APP_KEY-- это мастер-ключ для шифрования всех данных в приложении- Без него невозможно расшифровать cookies, сессии, зашифрованные поля
- При смене
APP_KEYвсе ранее зашифрованные данные станут нечитаемыми - Никогда не коммитьте
APP_KEYв Git
Ротация ключей
Laravel 11 поддерживает graceful-ротацию ключей:
APP_KEY=base64:new-key-here
APP_PREVIOUS_KEYS=base64:old-key-1,base64:old-key-2
При расшифровке Laravel сначала попробует текущий ключ, затем предыдущие. Новые данные всегда шифруются текущим ключом.
Использование шифрования
use Illuminate\Support\Facades\Crypt;
// Encrypt a value
$encrypted = Crypt::encryptString('secret-data');
// Decrypt a value
$decrypted = Crypt::decryptString($encrypted);
// Returns: 'secret-data'
// Encrypt any serializable value (not just strings)
$encrypted = Crypt::encrypt(['key' => 'value', 'nested' => true]);
$decrypted = Crypt::decrypt($encrypted);
// Returns: ['key' => 'value', 'nested' => true]
// Using helpers
$encrypted = encrypt('secret-data');
$decrypted = decrypt($encrypted);
Обработка ошибок шифрования
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
try {
$decrypted = Crypt::decryptString($encryptedValue);
} catch (DecryptException $e) {
// Invalid data, tampered value, wrong key, etc.
Log::error('Decryption failed', ['error' => $e->getMessage()]);
}
Шифрование полей модели
Laravel позволяет автоматически шифровать и расшифровывать атрибуты модели.
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class UserProfile extends Model
{
/**
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'ssn', // Social Security Number
'bank_account', // Bank account number
'notes',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
// Automatically encrypt/decrypt these attributes
'ssn' => 'encrypted',
'bank_account' => 'encrypted',
// Encrypted array
'notes' => 'encrypted:array',
// Encrypted collection
// 'metadata' => 'encrypted:collection',
// Encrypted object
// 'settings' => 'encrypted:object',
];
}
}
// Usage is transparent
$profile = UserProfile::create([
'user_id' => 1,
'ssn' => '123-45-6789', // Stored encrypted in DB
'bank_account' => 'GB82WEST', // Stored encrypted in DB
]);
echo $profile->ssn; // '123-45-6789' (automatically decrypted)
echo $profile->bank_account; // 'GB82WEST' (automatically decrypted)
// In the database, the values are encrypted strings
Важно: Зашифрованные поля нельзя использовать в WHERE-условиях, сортировке или индексах. Они хранятся как длинные зашифрованные строки.
Кастомный Encrypter
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Encryption\Encrypter;
final class FieldEncrypter
{
private Encrypter $encrypter;
public function __construct(string $key)
{
// Create an encrypter with a custom key
$this->encrypter = new Encrypter(
base64_decode($key),
'aes-256-gcm'
);
}
public function encrypt(string $value): string
{
return $this->encrypter->encryptString($value);
}
public function decrypt(string $value): string
{
return $this->encrypter->decryptString($value);
}
}
Хеширование (Hashing)
Хеширование -- необратимое преобразование данных. В Laravel используется преимущественно для паролей.
Поддерживаемые алгоритмы
// config/hashing.php
return [
'driver' => env('HASH_DRIVER', 'bcrypt'),
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12),
'verify' => true,
],
'argon' => [
'memory' => 65536, // 64 MB
'threads' => 1,
'time' => 4,
'verify' => true,
],
'argon2id' => [
'memory' => 65536,
'threads' => 1,
'time' => 4,
'verify' => true,
],
'rehash_on_login' => true,
];
Bcrypt
- Проверенный временем алгоритм (1999)
- Параметр
roundsконтролирует сложность (каждое увеличение на 1 удваивает время) - Рекомендуемое значение: 12 (по умолчанию в Laravel)
- Максимальная длина пароля: 72 байта
Argon2 / Argon2id
- Победитель Password Hashing Competition (2015)
- Параметры: memory, threads, time
- Argon2id -- гибрид, устойчивый к side-channel и GPU-атакам
- Рекомендуется для новых приложений
Использование Hash facade
use Illuminate\Support\Facades\Hash;
// Hash a password
$hashed = Hash::make('my-password');
// Hash with custom options
$hashed = Hash::make('my-password', [
'rounds' => 14, // For bcrypt
]);
$hashed = Hash::make('my-password', [
'memory' => 131072, // For argon2
'time' => 6,
'threads' => 2,
]);
// Check if a password matches a hash
if (Hash::check('my-password', $hashed)) {
// Password matches
}
// Check if a hash needs rehashing (algorithm or parameters changed)
if (Hash::needsRehash($hashed)) {
$hashed = Hash::make('my-password');
}
// Determine the hash driver for a hash
$info = Hash::info($hashed);
// Returns: ['algo' => 2, 'algoName' => 'bcrypt', 'options' => ['cost' => 12]]
Automatic Password Rehashing
Laravel 11 может автоматически перехешировать пароли при входе, если параметры хеширования изменились.
// config/hashing.php
'rehash_on_login' => true,
Это значит: если вы увеличили rounds с 10 до 12, при следующем входе пароль каждого пользователя будет перехеширован с новыми параметрами.
Кастомизация хеширования паролей в модели
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Foundation\Auth\User as Authenticatable;
final class User extends Authenticatable
{
/**
* Using 'hashed' cast: automatically hash when setting.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'password' => 'hashed',
];
}
// Or use a mutator (older approach)
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => Hash::make($value),
);
}
}
Проверка паролей
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
final class PasswordController extends Controller
{
public function update(Request $request): JsonResponse
{
$validated = $request->validate([
'current_password' => ['required'],
'new_password' => ['required', 'min:8', 'confirmed'],
]);
// Verify current password
if (! Hash::check($validated['current_password'], $request->user()->password)) {
return response()->json([
'message' => 'Текущий пароль неверный.',
], 422);
}
$request->user()->update([
'password' => $validated['new_password'], // 'hashed' cast handles hashing
]);
return response()->json([
'message' => 'Пароль успешно обновлён.',
]);
}
}
Правило текущего пароля
use Illuminate\Validation\Rules\Password;
$request->validate([
'current_password' => ['required', 'current_password'],
'new_password' => [
'required',
'confirmed',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // Check against Have I Been Pwned
],
]);
Сравнение шифрования и хеширования
| Характеристика | Шифрование (Encryption) | Хеширование (Hashing) |
|---|---|---|
| Обратимость | Обратимо (decrypt) | Необратимо |
| Назначение | Защита данных, которые нужно прочитать | Проверка данных без раскрытия |
| Пример | API ключи, персональные данные | Пароли |
| Алгоритм | AES-256-CBC/GCM | Bcrypt, Argon2id |
| Ключ | Требуется (APP_KEY) | Не требуется |
| Поиск в БД | Невозможен (разные ciphertext) | Невозможен |
Лучшие практики безопасности
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
final class SecurityService
{
/**
* NEVER compare passwords directly.
* ALWAYS use Hash::check().
*/
public function verifyPassword(string $plainPassword, string $hashedPassword): bool
{
// ✅ Correct: timing-safe comparison
return Hash::check($plainPassword, $hashedPassword);
// ❌ Wrong: vulnerable to timing attacks
// return $plainPassword === $hashedPassword;
}
/**
* NEVER store sensitive data in plain text.
* Use encryption for reversible protection.
*/
public function storeSensitiveData(string $data): string
{
return Crypt::encryptString($data);
}
/**
* NEVER hash secrets that need to be retrieved.
* Use encryption instead.
*/
public function storeApiKey(string $apiKey): string
{
// ✅ Correct: encryption for data that needs to be read
return Crypt::encryptString($apiKey);
// ❌ Wrong: hashing means you can never retrieve the key
// return Hash::make($apiKey);
}
/**
* ALWAYS use constant-time comparison for secrets.
*/
public function verifyToken(string $provided, string $stored): bool
{
return hash_equals($stored, $provided);
}
}
Тестирование
<?php
declare(strict_types=1);
namespace Tests\Unit;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use PHPUnit\Framework\TestCase;
final class SecurityTest extends TestCase
{
public function test_encryption_roundtrip(): void
{
$original = 'sensitive-data-123';
$encrypted = Crypt::encryptString($original);
$this->assertNotEquals($original, $encrypted);
$this->assertEquals($original, Crypt::decryptString($encrypted));
}
public function test_hashing_is_one_way(): void
{
$password = 'my-secure-password';
$hashed = Hash::make($password);
$this->assertNotEquals($password, $hashed);
$this->assertTrue(Hash::check($password, $hashed));
$this->assertFalse(Hash::check('wrong-password', $hashed));
}
public function test_same_password_produces_different_hashes(): void
{
$password = 'my-password';
$hash1 = Hash::make($password);
$hash2 = Hash::make($password);
// Different hashes due to random salt
$this->assertNotEquals($hash1, $hash2);
// But both verify correctly
$this->assertTrue(Hash::check($password, $hash1));
$this->assertTrue(Hash::check($password, $hash2));
}
}