Реализация шифрования персональных данных на сайте
Шифрование персональных данных (ПДн) в базе данных — обязательный элемент защиты при обработке чувствительной информации. Даже при компрометации БД злоумышленник получает зашифрованные данные, непригодные без ключей.
Что и как шифровать
Данные, требующие шифрования:
- ИНН, СНИЛС, серия/номер паспорта
- Медицинские данные, диагнозы
- Финансовые данные, номера карт (кроме PCI DSS — там отдельный стандарт)
- Биометрия
Данные, которые нужно только хэшировать (необратимо):
- Пароли → bcrypt, Argon2id
- Секретные токены, API-ключи
Данные, по которым нужен поиск:
- Требуют deterministic encryption или токенизации
Алгоритмы шифрования
AES-256-GCM — симметричное шифрование с аутентификацией. Стандарт для шифрования данных в покое.
RSA-OAEP — асимметричное. Используется для шифрования симметричных ключей или при необходимости шифровать данные без возможности расшифровки на сервере.
ChaCha20-Poly1305 — альтернатива AES на платформах без аппаратного ускорения AES.
Шифрование на уровне приложения (Laravel)
// Шифрование при записи / расшифровка при чтении через Casts
// app/Casts/EncryptedCast.php
class EncryptedCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): ?string
{
if (is_null($value)) return null;
try {
return Crypt::decryptString($value);
} catch (DecryptException) {
return null;
}
}
public function set($model, string $key, $value, array $attributes): ?string
{
if (is_null($value)) return null;
return Crypt::encryptString($value);
}
}
// В модели
class Patient extends Model
{
protected $casts = [
'passport_number' => EncryptedCast::class,
'medical_notes' => EncryptedCast::class,
'snils' => EncryptedCast::class,
];
}
// Использование — прозрачно для кода
$patient->passport_number = '4510 123456'; // автоматически шифруется
$decrypted = $patient->passport_number; // автоматически расшифровывается
Управление ключами
Хранить ключи шифрования в базе данных вместе с зашифрованными данными — бессмысленно. Варианты:
HashiCorp Vault:
// Получение ключа из Vault
$vault = new Vault([
'address' => 'https://vault.internal:8200',
'token' => env('VAULT_TOKEN'),
]);
$keyData = $vault->read('secret/data/app-encryption-key');
$encryptionKey = $keyData['data']['key'];
AWS KMS:
use Aws\Kms\KmsClient;
$kms = new KmsClient(['region' => 'eu-west-1']);
// Шифрование данных через KMS
$result = $kms->encrypt([
'KeyId' => 'arn:aws:kms:eu-west-1:123456:key/abc-123',
'Plaintext' => $sensitiveData,
]);
$encryptedData = base64_encode($result['CiphertextBlob']);
Envelope Encryption — лучшая практика: данные шифруются Data Encryption Key (DEK), DEK шифруется Key Encryption Key (KEK), KEK хранится в KMS/Vault.
Детерминированное шифрование для поиска
Обычный AES-GCM генерирует разный шифртекст для одного и того же значения. Поиск по зашифрованному полю невозможен. Решения:
Вариант 1: Хэш для поиска + шифрование для хранения:
class PersonalDataRepository
{
public function findByPassport(string $passport): ?Patient
{
// Поиск по детерминированному HMAC-хэшу
$hash = hash_hmac('sha256', $passport, config('app.search_key'));
return Patient::where('passport_hash', $hash)->first();
}
public function store(string $passport): void
{
Patient::create([
'passport_data' => Crypt::encryptString($passport), // для отображения
'passport_hash' => hash_hmac('sha256', $passport, config('app.search_key')), // для поиска
]);
}
}
Вариант 2: PostgreSQL pgcrypto:
-- Шифрование
INSERT INTO patients (passport)
VALUES (pgp_sym_encrypt('4510 123456', current_setting('app.encryption_key')));
-- Расшифровка
SELECT pgp_sym_decrypt(passport::bytea, current_setting('app.encryption_key'))
FROM patients WHERE id = 1;
Прозрачное шифрование базы данных (TDE)
Для PostgreSQL — pgcrypto на уровне колонок или FDE (Full Disk Encryption) на уровне сервера через LUKS. TDE шифрует данные на диске, но не защищает при наличии доступа к запущенной БД.
Ротация ключей
// Команда для ротации ключей шифрования
class RotateEncryptionKeyCommand extends Command
{
public function handle(): void
{
$oldKey = config('app.old_encryption_key');
$newKey = config('app.key');
Patient::chunk(100, function ($patients) use ($oldKey, $newKey) {
foreach ($patients as $patient) {
// Расшифровать старым ключом, зашифровать новым
$decrypted = Crypt::decryptString($patient->getRawOriginal('passport_number'));
$patient->updateQuietly([
'passport_number' => Crypt::encryptString($decrypted),
]);
}
});
}
}
Шифрование файлов
// Загружаемые документы — шифрование перед сохранением
class EncryptedFileStorage
{
public function store(UploadedFile $file): string
{
$content = file_get_contents($file->getPathname());
$encrypted = Crypt::encrypt($content);
$path = 'encrypted/' . Str::uuid() . '.enc';
Storage::put($path, $encrypted);
return $path;
}
public function retrieve(string $path): string
{
$encrypted = Storage::get($path);
return Crypt::decrypt($encrypted);
}
}
Аудит операций с ПДн
Каждое обращение к зашифрованным ПДн должно логироваться: кто, когда, какое поле расшифровал. Это требование 152-ФЗ при обработке биометрии и специальных категорий ПДн.
Срок реализации
- Шифрование на уровне модели (Cast) для основных полей: 2–3 дня
- Интеграция с Vault/KMS + envelope encryption: 5–7 дней
- Детерминированное шифрование + поиск: +3 дня
- Ротация ключей + аудит доступа: +2–3 дня







