MidТеория5 min

Шифрование и хеширование (Encryption & Hashing)

Шифрование (encrypt/decrypt), APP_KEY, хеширование (bcrypt, argon2), Hash facade, проверка паролей

Шифрование и хеширование (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));
    }
}

Проверь себя

Что делает Hash::needsRehash($hash)?

Почему два вызова Hash::make('password') возвращают РАЗНЫЕ хеши?

Можно ли использовать зашифрованные (encrypted) поля в WHERE-условиях SQL-запросов?

Какой каст для Eloquent-атрибута автоматически шифрует данные при записи и расшифровывает при чтении?

Что произойдёт с зашифрованными данными в базе, если сменить APP_KEY без использования APP_PREVIOUS_KEYS?