MidТеория14 min

Шифрование и TLS

Симметричное и асимметричное шифрование, TLS handshake, управление сертификатами, PHP: openssl и sodium

Основы криптографии

Тип Алгоритмы Применение
Симметричное AES-256-GCM, ChaCha20 Шифрование данных (быстро)
Асимметричное RSA, Ed25519, ECDSA Обмен ключами, подписи (медленно)
Хеширование SHA-256, SHA-3, BLAKE2 Проверка целостности (необратимо)
KDF Argon2id, bcrypt, scrypt Хеширование паролей (намеренно медленно)
HMAC HMAC-SHA256 Проверка подлинности сообщений

Симметричное vs Асимметричное

Симметричное Асимметричное
Ключи Один ключ Пара: public + private
Скорость Быстро (~1000x) Медленно
Проблема Как передать ключ? Вычислительная нагрузка
Использование Шифрование данных Обмен ключами, подписи

На практике используется гибридный подход: асимметричное шифрование для обмена симметричным ключом, далее все данные шифруются симметрично (именно так работает TLS).

Шифрование с openssl

<?php

declare(strict_types=1);

namespace App\Encryption;

final readonly class AesEncryptor
{
    private const CIPHER = 'aes-256-gcm';
    private const TAG_LENGTH = 16;

    public function __construct(
        private string $key, // 32 bytes for AES-256
    ) {
        if (strlen($this->key) !== 32) {
            throw new \InvalidArgumentException('Key must be exactly 32 bytes for AES-256');
        }
    }

    /**
     * Encrypt plaintext using AES-256-GCM.
     * Returns: IV (12 bytes) + Tag (16 bytes) + Ciphertext, base64-encoded.
     */
    public function encrypt(string $plaintext): string
    {
        // A GCM IV must never repeat under the same key — a single reuse
        // leaks the XOR of both plaintexts and breaks the authentication.
        // Always random, always per message
        $iv = random_bytes(12); // GCM recommended IV length
        $tag = '';

        $ciphertext = openssl_encrypt(
            $plaintext,
            self::CIPHER,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '', // AAD (Additional Authenticated Data)
            self::TAG_LENGTH,
        );

        if ($ciphertext === false) {
            throw new \RuntimeException('Encryption failed: ' . openssl_error_string());
        }

        // Concatenate IV + Tag + Ciphertext
        return base64_encode($iv . $tag . $ciphertext);
    }

    /**
     * Decrypt ciphertext encrypted with encrypt().
     */
    public function decrypt(string $encoded): string
    {
        $decoded = base64_decode($encoded, true);

        if ($decoded === false || strlen($decoded) < 12 + self::TAG_LENGTH) {
            throw new \InvalidArgumentException('Invalid ciphertext');
        }

        $iv = substr($decoded, 0, 12);
        $tag = substr($decoded, 12, self::TAG_LENGTH);
        $ciphertext = substr($decoded, 12 + self::TAG_LENGTH);

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::CIPHER,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
        );

        if ($plaintext === false) {
            throw new \RuntimeException('Decryption failed — data may be tampered');
        }

        return $plaintext;
    }

    /**
     * Generate a secure encryption key.
     */
    public static function generateKey(): string
    {
        return random_bytes(32);
    }
}
## Шифрование с Sodium

Sodium (libsodium) — современная криптографическая библиотека, встроенная в PHP 7.2+. Более безопасная, чем openssl, благодаря защите от типичных ошибок.

<?php

declare(strict_types=1);

namespace App\Encryption;

final readonly class SodiumEncryptor
{
    public function __construct(
        private string $key,
    ) {
        if (strlen($this->key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
            throw new \InvalidArgumentException(
                sprintf('Key must be %d bytes', SODIUM_CRYPTO_SECRETBOX_KEYBYTES),
            );
        }
    }

    /**
     * Encrypt using XSalsa20-Poly1305 (authenticated encryption).
     */
    public function encrypt(string $plaintext): string
    {
        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

        $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->key);

        // Nonce + Ciphertext
        $result = base64_encode($nonce . $ciphertext);

        // Clear sensitive data from memory
        sodium_memzero($plaintext);

        return $result;
    }

    /**
     * Decrypt data encrypted with encrypt().
     */
    public function decrypt(string $encoded): string
    {
        $decoded = base64_decode($encoded, true);

        if ($decoded === false) {
            throw new \InvalidArgumentException('Invalid base64');
        }

        $nonceSize = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;

        if (strlen($decoded) < $nonceSize) {
            throw new \InvalidArgumentException('Invalid ciphertext');
        }

        $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 — data tampered or wrong key');
        }

        return $plaintext;
    }

    /**
     * Generate a key for symmetric encryption.
     */
    public static function generateKey(): string
    {
        return sodium_crypto_secretbox_keygen();
    }

    /**
     * Asymmetric encryption: encrypt for a recipient's public key.
     */
    public static function encryptForRecipient(string $message, string $recipientPublicKey): string
    {
        $sealed = sodium_crypto_box_seal($message, $recipientPublicKey);

        return base64_encode($sealed);
    }

    /**
     * Asymmetric decryption: decrypt with own keypair.
     */
    public static function decryptWithKeypair(string $encoded, string $keypair): string
    {
        $decoded = base64_decode($encoded, true);

        if ($decoded === false) {
            throw new \InvalidArgumentException('Invalid base64');
        }

        $plaintext = sodium_crypto_box_seal_open($decoded, $keypair);

        if ($plaintext === false) {
            // Do not report whether the key or the ciphertext was at fault
            throw new \RuntimeException('Decryption failed');
        }

        return $plaintext;
    }
}
## TLS (Transport Layer Security)

TLS шифрует данные в транзите между клиентом и сервером.

TLS 1.3 Handshake

Client                              Server
  |                                    |
  |--- ClientHello ----------------->  |  (supported ciphers, key share)
  |                                    |
  |<--- ServerHello + Certificate ---  |  (chosen cipher, key share, cert)
  |<--- Finished ------------------   |
  |                                    |
  |--- Finished ------------------>   |  (verify server cert)
  |                                    |
  |<=== Encrypted Application Data ==>|

TLS 1.3 vs TLS 1.2:

  • 1-RTT handshake (вместо 2-RTT)
  • 0-RTT resumption для повторных подключений
  • Убраны устаревшие шифры (RSA key exchange, CBC)
  • Только AEAD cipher suites (AES-GCM, ChaCha20-Poly1305)

Рекомендации по TLS

Настройка Рекомендация
Версия TLS 1.3 (минимум TLS 1.2)
Cipher suites AES-256-GCM, ChaCha20-Poly1305
Ключ сертификата ECDSA P-256 или Ed25519
HSTS Включить с max-age >= 1 год
OCSP Stapling Включить для проверки сертификатов
Certificate Transparency Мониторить выпуск сертификатов

Управление сертификатами

Аспект Описание
Let's Encrypt Бесплатные сертификаты, автообновление
Срок действия 90 дней (Let's Encrypt), автообновление certbot
Wildcard *.example.com — один сертификат на все поддомены
mTLS Mutual TLS — обе стороны предъявляют сертификат
Certificate Pinning Привязка к конкретному сертификату (мобильные приложения)

Encryption at Rest vs in Transit

At Rest In Transit
Что Данные на диске / в БД Данные по сети
Как AES-256, LUKS, column encryption TLS 1.3
Где БД, файловая система, бэкапы HTTP, gRPC, message queues
Зачем Защита от кражи дисков/бэкапов Защита от MITM
<?php

declare(strict_types=1);

namespace App\Encryption;

/**
 * Column-level encryption for sensitive database fields.
 */
final readonly class ColumnEncryptor
{
    public function __construct(
        private SodiumEncryptor $encryptor,
    ) {}

    /**
     * Encrypt sensitive fields before database storage.
     *
     * @param array<string, mixed> $data
     * @param array<string> $sensitiveFields Fields to encrypt
     * @return array<string, mixed>
     */
    public function encryptFields(array $data, array $sensitiveFields): array
    {
        foreach ($sensitiveFields as $field) {
            if (isset($data[$field]) && is_string($data[$field])) {
                $data[$field] = $this->encryptor->encrypt($data[$field]);
            }
        }

        return $data;
    }

    /**
     * Decrypt sensitive fields after database retrieval.
     *
     * @param array<string, mixed> $data
     * @param array<string> $sensitiveFields
     * @return array<string, mixed>
     */
    public function decryptFields(array $data, array $sensitiveFields): array
    {
        foreach ($sensitiveFields as $field) {
            if (isset($data[$field]) && is_string($data[$field])) {
                $data[$field] = $this->encryptor->decrypt($data[$field]);
            }
        }

        return $data;
    }
}

// Usage: encrypt SSN, phone, etc. before INSERT
$encrypted = $columnEncryptor->encryptFields(
    data: ['name' => 'Alice', 'ssn' => '123-45-6789', 'phone' => '+1234567890'],
    sensitiveFields: ['ssn', 'phone'],
);
// name: "Alice", ssn: "base64(encrypted)", phone: "base64(encrypted)"
## Итоги
Концепция Суть
AES-256-GCM Стандарт симметричного шифрования
Sodium Современная библиотека, встроена в PHP
TLS 1.3 Шифрование in transit, 1-RTT handshake
Encryption at rest Защита данных на диске
Column encryption Шифрование отдельных полей в БД
Key management Ключи отдельно от данных, ротация

Правило: Используйте Sodium вместо openssl для новых проектов. Sodium имеет более безопасный API и защищает от типичных криптографических ошибок.