На практике используется гибридный подход: асимметричное шифрование для обмена симметричным ключом, далее все данные шифруются симметрично (именно так работает 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);
}
}
package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
)
// AESEncryptor provides AES-256-GCM encryption.
type AESEncryptor struct {
key []byte // 32 bytes for AES-256
}
// NewAESEncryptor creates an encryptor with the given 32-byte key.
func NewAESEncryptor(key []byte) (*AESEncryptor, error) {
if len(key) != 32 {
return nil, errors.New("key must be exactly 32 bytes for AES-256")
}
return &AESEncryptor{key: key}, nil
}
// Encrypt encrypts plaintext using AES-256-GCM.
func (e *AESEncryptor) Encrypt(plaintext []byte) (string, error) {
block, err := aes.NewCipher(e.key)
if err != nil {
return "", fmt.Errorf("create cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("create GCM: %w", err)
}
// A GCM nonce 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.
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", fmt.Errorf("generate nonce: %w", err)
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts data encrypted with Encrypt.
func (e *AESEncryptor) Decrypt(encoded string) ([]byte, error) {
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("base64 decode: %w", err)
}
block, err := aes.NewCipher(e.key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}
return gcm.Open(nil, data[:nonceSize], data[nonceSize:], nil)
}
// GenerateKey generates a random 32-byte AES-256 key.
func GenerateKey() ([]byte, error) {
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
// Return nil rather than a partially filled key: a caller that
// ignores the error must not end up encrypting under zero bytes.
return nil, fmt.Errorf("generate key: %w", err)
}
return key, nil
}
using System.Security.Cryptography;
namespace App.Encryption;
// AES-256-GCM: authenticated encryption, so tampering is detected on decrypt.
public sealed class AesEncryptor : IDisposable
{
private const int KeySize = 32; // AES-256
private const int NonceSize = 12; // GCM recommended nonce length
private const int TagSize = 16;
private readonly AesGcm _aes;
public AesEncryptor(byte[] key)
{
if (key.Length != KeySize)
{
throw new ArgumentException($"Key must be exactly {KeySize} bytes for AES-256", nameof(key));
}
_aes = new AesGcm(key, TagSize);
}
// Returns nonce + tag + ciphertext, base64-encoded.
public string Encrypt(ReadOnlySpan<byte> plaintext)
{
byte[] output = new byte[NonceSize + TagSize + plaintext.Length];
Span<byte> nonce = output.AsSpan(0, NonceSize);
Span<byte> tag = output.AsSpan(NonceSize, TagSize);
Span<byte> ciphertext = output.AsSpan(NonceSize + TagSize);
// A nonce must never repeat under the same key — always random per message.
RandomNumberGenerator.Fill(nonce);
_aes.Encrypt(nonce, plaintext, ciphertext, tag);
return Convert.ToBase64String(output);
}
public byte[] Decrypt(string encoded)
{
byte[] data = Convert.FromBase64String(encoded);
if (data.Length < NonceSize + TagSize)
{
throw new ArgumentException("Ciphertext too short", nameof(encoded));
}
ReadOnlySpan<byte> nonce = data.AsSpan(0, NonceSize);
ReadOnlySpan<byte> tag = data.AsSpan(NonceSize, TagSize);
ReadOnlySpan<byte> ciphertext = data.AsSpan(NonceSize + TagSize);
byte[] plaintext = new byte[ciphertext.Length];
try
{
_aes.Decrypt(nonce, ciphertext, tag, plaintext);
}
catch (AuthenticationTagMismatchException exc)
{
throw new CryptographicException("Decryption failed — data may be tampered", exc);
}
return plaintext;
}
// Generate a secure AES-256 key.
public static byte[] GenerateKey() => RandomNumberGenerator.GetBytes(KeySize);
public void Dispose() => _aes.Dispose();
}
import base64
import os
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
KEY_SIZE = 32 # AES-256
NONCE_SIZE = 12 # GCM recommended nonce length
class DecryptionError(Exception):
"""Raised when authentication fails — wrong key or tampered ciphertext."""
class AesEncryptor:
"""AES-256-GCM: authenticated encryption, so tampering is detected on decrypt."""
def __init__(self, key: bytes) -> None:
if len(key) != KEY_SIZE:
raise ValueError(f"key must be exactly {KEY_SIZE} bytes for AES-256")
self._aesgcm = AESGCM(key)
def encrypt(self, plaintext: bytes) -> str:
"""Return nonce + ciphertext (tag appended), base64-encoded."""
# A nonce must never repeat under the same key — always random per message.
nonce = os.urandom(NONCE_SIZE)
ciphertext = self._aesgcm.encrypt(nonce, plaintext, associated_data=None)
return base64.b64encode(nonce + ciphertext).decode("ascii")
def decrypt(self, encoded: str) -> bytes:
data = base64.b64decode(encoded, validate=True)
if len(data) < NONCE_SIZE:
raise ValueError("ciphertext too short")
nonce, ciphertext = data[:NONCE_SIZE], data[NONCE_SIZE:]
try:
return self._aesgcm.decrypt(nonce, ciphertext, associated_data=None)
except InvalidTag as exc:
raise DecryptionError("decryption failed — data may be tampered") from exc
@staticmethod
def generate_key() -> bytes:
"""Generate a secure AES-256 key."""
return AESGCM.generate_key(bit_length=256)
## Шифрование с 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;
}
}
package encryption
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/nacl/box"
"golang.org/x/crypto/nacl/secretbox"
)
const (
keySize = 32
nonceSize = 24
)
// SecretBoxEncryptor uses NaCl secretbox (XSalsa20-Poly1305).
type SecretBoxEncryptor struct {
key [keySize]byte
}
// NewSecretBoxEncryptor creates an encryptor from a 32-byte key.
func NewSecretBoxEncryptor(key [keySize]byte) *SecretBoxEncryptor {
return &SecretBoxEncryptor{key: key}
}
// Encrypt encrypts plaintext using XSalsa20-Poly1305.
func (e *SecretBoxEncryptor) Encrypt(plaintext []byte) (string, error) {
var nonce [nonceSize]byte
if _, err := rand.Read(nonce[:]); err != nil {
return "", err
}
sealed := secretbox.Seal(nonce[:], plaintext, &nonce, &e.key)
return base64.StdEncoding.EncodeToString(sealed), nil
}
// Decrypt decrypts data encrypted with Encrypt.
func (e *SecretBoxEncryptor) Decrypt(encoded string) ([]byte, error) {
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, err
}
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}
var nonce [nonceSize]byte
copy(nonce[:], data[:nonceSize])
plaintext, ok := secretbox.Open(nil, data[nonceSize:], &nonce, &e.key)
if !ok {
return nil, errors.New("decryption failed -- data tampered or wrong key")
}
return plaintext, nil
}
// GenerateSecretBoxKey generates a random key for secretbox.
func GenerateSecretBoxKey() ([keySize]byte, error) {
var key [keySize]byte
if _, err := rand.Read(key[:]); err != nil {
// Swallowing this error would hand back an all-zero key and
// encrypt every message under a constant an attacker can guess.
return key, fmt.Errorf("generate key: %w", err)
}
return key, nil
}
// EncryptForRecipient seals a message for a recipient's public key.
// Anyone can seal; only the holder of the private key can open it.
func EncryptForRecipient(message []byte, recipientPublicKey *[keySize]byte) (string, error) {
sealed, err := box.SealAnonymous(nil, message, recipientPublicKey, rand.Reader)
if err != nil {
return "", fmt.Errorf("seal message: %w", err)
}
return base64.StdEncoding.EncodeToString(sealed), nil
}
// DecryptWithKeypair opens a message produced by EncryptForRecipient.
func DecryptWithKeypair(encoded string, publicKey, privateKey *[keySize]byte) ([]byte, error) {
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("base64 decode: %w", err)
}
plaintext, ok := box.OpenAnonymous(nil, data, publicKey, privateKey)
if !ok {
// Do not report whether the key or the ciphertext was at fault.
return nil, errors.New("decryption failed")
}
return plaintext, nil
}
using System.Security.Cryptography;
using NSec.Cryptography;
namespace App.Encryption;
// .NET has no built-in libsodium, so NSec.Cryptography (a managed binding)
// provides the same misuse-resistant primitives.
public sealed class SecretBoxEncryptor(Key key) : IDisposable
{
private const int NonceSize = 24; // XChaCha20-Poly1305 nonce
private static readonly AeadAlgorithm Algorithm = AeadAlgorithm.XChaCha20Poly1305;
// Encrypt using XChaCha20-Poly1305 (authenticated encryption).
public string Encrypt(ReadOnlySpan<byte> plaintext)
{
Span<byte> nonce = stackalloc byte[NonceSize];
RandomNumberGenerator.Fill(nonce);
byte[] ciphertext = Algorithm.Encrypt(key, nonce, associatedData: default, plaintext);
byte[] result = new byte[NonceSize + ciphertext.Length];
nonce.CopyTo(result);
ciphertext.CopyTo(result.AsSpan(NonceSize));
return Convert.ToBase64String(result);
}
public byte[] Decrypt(string encoded)
{
byte[] data = Convert.FromBase64String(encoded);
if (data.Length < NonceSize)
{
throw new ArgumentException("Ciphertext too short", nameof(encoded));
}
// Decrypt returns false rather than throwing on a bad tag.
if (!Algorithm.Decrypt(
key,
data.AsSpan(0, NonceSize),
associatedData: default,
data.AsSpan(NonceSize),
out byte[]? plaintext))
{
throw new CryptographicException("Decryption failed — data tampered or wrong key");
}
return plaintext;
}
// Generate a key for symmetric encryption.
public static Key GenerateKey()
=> Key.Create(Algorithm, new KeyCreationParameters
{
ExportPolicy = KeyExportPolicies.AllowPlaintextArchiving,
});
public void Dispose() => key.Dispose();
}
import base64
import nacl.secret
import nacl.utils
from nacl.exceptions import CryptoError
from nacl.public import PublicKey, SealedBox
class DecryptionError(Exception):
"""Raised when authentication fails — wrong key or tampered ciphertext."""
class SecretBoxEncryptor:
"""PyNaCl secretbox: XSalsa20-Poly1305 authenticated encryption."""
def __init__(self, key: bytes) -> None:
if len(key) != nacl.secret.SecretBox.KEY_SIZE:
raise ValueError(f"key must be {nacl.secret.SecretBox.KEY_SIZE} bytes")
self._box = nacl.secret.SecretBox(key)
def encrypt(self, plaintext: bytes) -> str:
# PyNaCl generates and prepends a random nonce automatically.
encrypted = self._box.encrypt(plaintext)
return base64.b64encode(encrypted).decode("ascii")
def decrypt(self, encoded: str) -> bytes:
data = base64.b64decode(encoded, validate=True)
try:
return self._box.decrypt(data)
except CryptoError as exc:
raise DecryptionError("decryption failed — data tampered or wrong key") from exc
@staticmethod
def generate_key() -> bytes:
"""Generate a key for symmetric encryption."""
return nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
def encrypt_for_recipient(message: bytes, recipient_public_key: bytes) -> str:
"""Asymmetric encryption: anyone can seal, only the key holder can open."""
sealed = SealedBox(PublicKey(recipient_public_key)).encrypt(message)
return base64.b64encode(sealed).decode("ascii")
def decrypt_with_keypair(encoded: str, private_key: nacl.public.PrivateKey) -> bytes:
"""Asymmetric decryption with one's own keypair."""
try:
return SealedBox(private_key).decrypt(base64.b64decode(encoded, validate=True))
except CryptoError as exc:
raise DecryptionError("decryption failed") from exc
## TLS (Transport Layer Security)
TLS шифрует данные в транзите между клиентом и сервером.